Merge branch 'main' into feat/dino-mechanics

pull/277/head
Allison Ryan 3 years ago
commit f9b312225e

@ -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

@ -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"

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -6,8 +6,8 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template android_acres} /// {@template android_acres}
/// Area positioned on the left side of the board containing the [Spaceship], /// Area positioned on the left side of the board containing the
/// [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s. /// [AndroidSpaceship], [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s.
/// {@endtemplate} /// {@endtemplate}
class AndroidAcres extends Blueprint { class AndroidAcres extends Blueprint {
/// {@macro android_acres} /// {@macro android_acres}
@ -16,18 +16,23 @@ class AndroidAcres extends Blueprint {
components: [ components: [
AndroidBumper.a( AndroidBumper.a(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20000),
], ],
)..initialPosition = Vector2(-32.52, -9.1), )..initialPosition = Vector2(-25, 1.3),
AndroidBumper.b( AndroidBumper.b(
children: [
ScoringBehavior(points: 20000),
],
)..initialPosition = Vector2(-32.6, -9.2),
AndroidBumper.cow(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20),
], ],
)..initialPosition = Vector2(-22.89, -17.35), )..initialPosition = Vector2(-20.5, -13.8),
], ],
blueprints: [ blueprints: [
SpaceshipRamp(), SpaceshipRamp(),
Spaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
SpaceshipRail(), SpaceshipRail(),
], ],
); );

@ -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<void> 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<void> 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<void> 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]);
}
}

@ -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<void> 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]);
}
}

@ -1,13 +1,15 @@
export 'android_acres.dart'; export 'android_acres.dart';
export 'board.dart'; export 'bottom_group.dart';
export 'camera_controller.dart'; export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
export 'controlled_plunger.dart'; export 'controlled_plunger.dart';
export 'dino_desert.dart';
export 'drain.dart';
export 'flutter_forest/flutter_forest.dart'; export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_flow_controller.dart';
export 'google_word/google_word.dart'; export 'google_word/google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'multipliers/multipliers.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';
export 'sparky_fire_zone.dart'; export 'sparky_fire_zone.dart';
export 'wall.dart';

@ -1,3 +1,5 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.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(),
],
);
}

@ -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();
}
}

@ -7,7 +7,7 @@ import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template flutter_forest} /// {@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. /// off [DashNestBumper]s.
/// {@endtemplate} /// {@endtemplate}
class FlutterForest extends Component { class FlutterForest extends Component {
@ -23,17 +23,17 @@ class FlutterForest extends Component {
)..initialPosition = Vector2(8.35, -58.3), )..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main( DashNestBumper.main(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 200000),
], ],
)..initialPosition = Vector2(18.55, -59.35), )..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a( DashNestBumper.a(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20000),
], ],
)..initialPosition = Vector2(8.95, -51.95), )..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b( DashNestBumper.b(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20000),
], ],
)..initialPosition = Vector2(23.3, -46.75), )..initialPosition = Vector2(23.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66), DashAnimatronic()..position = Vector2(20, -66),

@ -1,6 +1,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; import 'package:pinball/game/components/google_word/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template google_word} /// {@template google_word}
@ -12,12 +13,30 @@ class GoogleWord extends Component {
required Vector2 position, required Vector2 position,
}) : super( }) : super(
children: [ children: [
GoogleLetter(0)..initialPosition = position + Vector2(-12.92, 1.82), GoogleLetter(
GoogleLetter(1)..initialPosition = position + Vector2(-8.33, -0.65), 0,
GoogleLetter(2)..initialPosition = position + Vector2(-2.88, -1.75), children: [ScoringBehavior(points: 5000)],
GoogleLetter(3)..initialPosition = position + Vector2(2.88, -1.75), )..initialPosition = position + Vector2(-12.92, 1.82),
GoogleLetter(4)..initialPosition = position + Vector2(8.33, -0.65), GoogleLetter(
GoogleLetter(5)..initialPosition = position + Vector2(12.92, 1.82), 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(), GoogleWordBonusBehavior(),
], ],
); );

@ -12,9 +12,9 @@ class Launcher extends Blueprint {
Launcher() Launcher()
: super( : super(
components: [ components: [
ControlledPlunger(compressionDistance: 14) ControlledPlunger(compressionDistance: 10.5)
..initialPosition = Vector2(40.7, 38), ..initialPosition = Vector2(41.1, 43),
RocketSpriteComponent()..position = Vector2(43, 62), RocketSpriteComponent()..position = Vector2(43, 62.3),
], ],
blueprints: [LaunchRamp()], blueprints: [LaunchRamp()],
); );

@ -0,0 +1 @@
export '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<PinballGame>,
ParentIsA<Multipliers>,
BlocComponent<GameBloc, GameState> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.multiplier != newState.multiplier;
}
@override
void onNewState(GameState state) {
final multipliers = parent.children.whereType<Multiplier>();
for (final multiplier in multipliers) {
multiplier.bloc.next(state.multiplier);
}
}
}

@ -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();
}

@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_fire_zone} /// {@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. /// can bounce off [SparkyBumper]s.
/// ///
/// When a [Ball] hits [SparkyBumper]s, the bumper animates. /// When a [Ball] hits [SparkyBumper]s, the bumper animates.
@ -18,17 +18,17 @@ class SparkyFireZone extends Blueprint {
components: [ components: [
SparkyBumper.a( SparkyBumper.a(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20000),
], ],
)..initialPosition = Vector2(-22.9, -41.65), )..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b( SparkyBumper.b(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20000),
], ],
)..initialPosition = Vector2(-21.25, -57.9), )..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c( SparkyBumper.c(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20000),
], ],
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8), SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8),
@ -47,7 +47,13 @@ class SparkyFireZone extends Blueprint {
class SparkyComputerSensor extends BodyComponent class SparkyComputerSensor extends BodyComponent
with InitialPosition, ContactCallbacks { with InitialPosition, ContactCallbacks {
/// {@macro sparky_computer_sensor} /// {@macro sparky_computer_sensor}
SparkyComputerSensor() : super(renderBody: false); SparkyComputerSensor()
: super(
renderBody: false,
children: [
ScoringBehavior(points: 200000),
],
);
@override @override
Body createBody() { Body createBody() {

@ -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();
}
}

@ -50,39 +50,42 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.boundary.bottom.keyName), images.load(components.Assets.images.boundary.bottom.keyName),
images.load(components.Assets.images.boundary.outer.keyName), images.load(components.Assets.images.boundary.outer.keyName),
images.load(components.Assets.images.boundary.outerBottom.keyName), images.load(components.Assets.images.boundary.outerBottom.keyName),
images.load(components.Assets.images.spaceship.saucer.keyName), images.load(components.Assets.images.android.spaceship.saucer.keyName),
images.load(components.Assets.images.spaceship.bridge.keyName), images
images.load(components.Assets.images.spaceship.ramp.boardOpening.keyName), .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( images.load(
components.Assets.images.spaceship.ramp.railingForeground.keyName, components.Assets.images.android.ramp.railingForeground.keyName,
), ),
images.load( 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.android.ramp.main.keyName),
images images.load(components.Assets.images.android.ramp.arrow.inactive.keyName),
.load(components.Assets.images.spaceship.ramp.arrow.inactive.keyName),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active1.keyName, components.Assets.images.android.ramp.arrow.active1.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active2.keyName, components.Assets.images.android.ramp.arrow.active2.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active3.keyName, components.Assets.images.android.ramp.arrow.active3.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active4.keyName, components.Assets.images.android.ramp.arrow.active4.keyName,
), ),
images.load( 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.android.rail.main.keyName),
images.load(components.Assets.images.spaceship.rail.exit.keyName), images.load(components.Assets.images.android.rail.exit.keyName),
images.load(components.Assets.images.androidBumper.a.lit.keyName), images.load(components.Assets.images.android.bumper.a.lit.keyName),
images.load(components.Assets.images.androidBumper.a.dimmed.keyName), images.load(components.Assets.images.android.bumper.a.dimmed.keyName),
images.load(components.Assets.images.androidBumper.b.lit.keyName), images.load(components.Assets.images.android.bumper.b.lit.keyName),
images.load(components.Assets.images.androidBumper.b.dimmed.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.top.keyName),
images.load(components.Assets.images.sparky.computer.base.keyName), images.load(components.Assets.images.sparky.computer.base.keyName),
images.load(components.Assets.images.sparky.animatronic.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.letter5.keyName),
images.load(components.Assets.images.googleWord.letter6.keyName), images.load(components.Assets.images.googleWord.letter6.keyName),
images.load(components.Assets.images.backboard.display.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(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName),

@ -6,6 +6,7 @@ import 'package:flame/game.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
@ -18,7 +19,8 @@ class PinballGame extends Forge2DGame
with with
FlameBloc, FlameBloc,
HasKeyboardHandlerComponents, HasKeyboardHandlerComponents,
Controls<_GameBallsController> { Controls<_GameBallsController>,
TapDetector {
PinballGame({ PinballGame({
required this.characterTheme, required this.characterTheme,
required this.audio, required this.audio,
@ -44,20 +46,18 @@ class PinballGame extends Forge2DGame
unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this))); unawaited(add(CameraController(this)));
unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); unawaited(add(Backboard.waiting(position: Vector2(0, -88))));
await add(Drain());
// TODO(allisonryan0002): banish Wall and Board classes in later PR. await add(BottomGroup());
await add(BottomWall());
unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp()));
final launcher = Launcher(); final launcher = Launcher();
unawaited(addFromBlueprint(launcher)); unawaited(addFromBlueprint(launcher));
unawaited(add(Board())); await add(Multipliers());
await add(FlutterForest());
await addFromBlueprint(SparkyFireZone()); await addFromBlueprint(SparkyFireZone());
await addFromBlueprint(AndroidAcres()); await addFromBlueprint(AndroidAcres());
await addFromBlueprint(DinoDesert());
unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls()));
await add(ChromeDino()..initialPosition = Vector2(12.3, -6.9));
await add( await add(
GoogleWord( GoogleWord(
position: Vector2( position: Vector2(
@ -67,9 +67,64 @@ class PinballGame extends Forge2DGame
), ),
); );
controller.attachTo(launcher.components.whereType<Plunger>().first); controller.attachTo(launcher.components.whereType<Plunger>().single);
await super.onLoad(); await super.onLoad();
} }
BoardSide? focusedBoardSide;
@override
void onTapDown(TapDownInfo info) {
if (info.raw.kind == PointerDeviceKind.touch) {
final rocket = children.whereType<RocketSpriteComponent>().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<Plunger>().first.pull();
} else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right;
final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide;
});
flippers.first.moveUp();
}
}
super.onTapDown(info);
}
@override
void onTapUp(TapUpInfo info) {
final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size;
if (bounds.contains(info.eventPosition.game.toOffset())) {
children.whereType<Plunger>().first.release();
} else {
_moveFlippersDown();
}
super.onTapUp(info);
}
@override
void onTapCancel() {
children.whereType<Plunger>().first.release();
_moveFlippersDown();
super.onTapCancel();
}
void _moveFlippersDown() {
if (focusedBoardSide != null) {
final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide;
});
flippers.first.moveDown();
focusedBoardSide = null;
}
}
} }
class _GameBallsController extends ComponentController<PinballGame> class _GameBallsController extends ComponentController<PinballGame>
@ -116,7 +171,7 @@ class _GameBallsController extends ComponentController<PinballGame>
} }
} }
class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { class DebugPinballGame extends PinballGame with FPSCounter {
DebugPinballGame({ DebugPinballGame({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
required PinballAudio audio, required PinballAudio audio,
@ -153,9 +208,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector {
@override @override
void onTapUp(TapUpInfo info) { void onTapUp(TapUpInfo info) {
add( super.onTapUp(info);
ControlledBall.debug()..initialPosition = info.eventPosition.game,
); if (info.raw.kind == PointerDeviceKind.mouse) {
add(ControlledBall.debug()..initialPosition = info.eventPosition.game);
}
} }
} }

@ -66,14 +66,14 @@ class _ScoreViewDecoration extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const radius = BorderRadius.all(Radius.circular(12)); const radius = BorderRadius.all(Radius.circular(12));
const boardWidth = 5.0; const borderWidth = 5.0;
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: radius, borderRadius: radius,
border: Border.all( border: Border.all(
color: AppColors.white, color: AppColors.white,
width: boardWidth, width: borderWidth,
), ),
image: DecorationImage( image: DecorationImage(
fit: BoxFit.cover, fit: BoxFit.cover,
@ -83,7 +83,7 @@ class _ScoreViewDecoration extends StatelessWidget {
), ),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(boardWidth - 1), padding: const EdgeInsets.all(borderWidth - 1),
child: ClipRRect( child: ClipRRect(
borderRadius: radius, borderRadius: radius,
child: child, child: child,

@ -28,6 +28,7 @@ class PlayButtonOverlay extends StatelessWidget {
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (_) { builder: (_) {
// TODO(arturplaczek): remove after merge StarBlocListener
final height = MediaQuery.of(context).size.height * 0.5; final height = MediaQuery.of(context).size.height * 0.5;
return Center( return Center(

@ -21,7 +21,7 @@ class RoundCountDisplay extends StatelessWidget {
Text( Text(
l10n.rounds, l10n.rounds,
style: AppTextStyle.subtitle1.copyWith( style: AppTextStyle.subtitle1.copyWith(
color: AppColors.orange, color: AppColors.yellow,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -53,7 +53,7 @@ class RoundIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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; const size = 8.0;
return Padding( return Padding(

@ -59,7 +59,7 @@ class _ScoreDisplay extends StatelessWidget {
Text( Text(
l10n.score.toLowerCase(), l10n.score.toLowerCase(),
style: AppTextStyle.subtitle1.copyWith( style: AppTextStyle.subtitle1.copyWith(
color: AppColors.orange, color: AppColors.yellow,
), ),
), ),
const _ScoreText(), const _ScoreText(),

@ -47,6 +47,14 @@ class $AssetsImagesComponentsGen {
/// File path: assets/images/components/background.png /// File path: assets/images/components/background.png
AssetGenImage get background => AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png'); 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 { class $AssetsImagesScoreGen {

@ -8,6 +8,10 @@
"@howToPlay": { "@howToPlay": {
"description": "Text displayed on the landing page how to play button" "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": "Launch Controls",
"@launchControls": { "@launchControls": {
"description": "Text displayed on the how to play dialog with the launch controls" "description": "Text displayed on the how to play dialog with the launch controls"
@ -16,6 +20,26 @@
"@flipperControls": { "@flipperControls": {
"description": "Text displayed on the how to play dialog with the flipper controls" "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": "Start",
"@start": { "@start": {
"description": "Text displayed on the character selection page start button" "description": "Text displayed on the character selection page start button"
@ -24,6 +48,10 @@
"@select": { "@select": {
"description": "Text displayed on the character selection page select button" "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": "Choose your character!",
"@characterSelectionTitle": { "@characterSelectionTitle": {
"description": "Title text displayed on the character selection page" "description": "Title text displayed on the character selection page"

@ -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<LeaderboardEvent, LeaderboardState> {
/// {@macro leaderboard_bloc}
LeaderboardBloc(this._leaderboardRepository)
: super(const LeaderboardState.initial()) {
on<Top10Fetched>(_onTop10Fetched);
on<LeaderboardEntryAdded>(_onLeaderboardEntryAdded);
}
final LeaderboardRepository _leaderboardRepository;
Future<void> _onTop10Fetched(
Top10Fetched event,
Emitter<LeaderboardState> emit,
) async {
emit(state.copyWith(status: LeaderboardStatus.loading));
try {
final top10Leaderboard =
await _leaderboardRepository.fetchTop10Leaderboard();
final leaderboardEntries = <LeaderboardEntry>[];
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<void> _onLeaderboardEntryAdded(
LeaderboardEntryAdded event,
Emitter<LeaderboardState> 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);
}
}
}

@ -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<Object?> 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<Object?> get props => [entry];
}

@ -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<LeaderboardEntry> leaderboard;
@override
List<Object> get props => [status, ranking, leaderboard];
LeaderboardState copyWith({
LeaderboardStatus? status,
LeaderboardRanking? ranking,
List<LeaderboardEntry>? leaderboard,
}) {
return LeaderboardState(
status: status ?? this.status,
ranking: ranking ?? this.ranking,
leaderboard: leaderboard ?? this.leaderboard,
);
}
}

@ -1,3 +0,0 @@
export 'bloc/leaderboard_bloc.dart';
export 'models/leader_board_entry.dart';
export 'view/leaderboard_page.dart';

@ -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<void>(
builder: (_) => LeaderboardPage(theme: theme),
);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LeaderboardBloc(
context.read<LeaderboardRepository>(),
)..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<LeaderboardBloc, LeaderboardState>(
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<void>(
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<LeaderboardEntry> 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<LeaderboardEntry> 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(),
),
),
);
}
}

@ -6,6 +6,7 @@ import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import 'package:pinball_ui/pinball_ui.dart';
class CharacterSelectionDialog extends StatelessWidget { class CharacterSelectionDialog extends StatelessWidget {
const CharacterSelectionDialog({Key? key}) : super(key: key); const CharacterSelectionDialog({Key? key}) : super(key: key);
@ -32,25 +33,31 @@ class CharacterSelectionView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return Scaffold( return PixelatedDecoration(
header: Text(
l10n.characterSelectionTitle,
style: Theme.of(context).textTheme.headline3,
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(height: 80),
Text(
l10n.characterSelectionTitle,
style: Theme.of(context).textTheme.headline3,
),
const SizedBox(height: 80),
const _CharacterSelectionGridView(), const _CharacterSelectionGridView(),
const SizedBox(height: 20), const SizedBox(height: 20),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
// TODO(arturplaczek): remove after merge StarBlocListener
final height = MediaQuery.of(context).size.height * 0.5;
showDialog<void>( showDialog<void>(
context: context, context: context,
builder: (_) => const HowToPlayDialog(), builder: (_) => Center(
child: SizedBox(
height: height,
width: height * 1.4,
child: HowToPlayDialog(),
),
),
); );
}, },
child: Text(l10n.start), child: Text(l10n.start),

@ -1,36 +1,236 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.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 StatefulWidget {
HowToPlayDialog({
Key? key,
@visibleForTesting PlatformHelper? platformHelper,
}) : platformHelper = platformHelper ?? PlatformHelper(),
super(key: key);
class HowToPlayDialog extends StatelessWidget { final PlatformHelper platformHelper;
const HowToPlayDialog({Key? key}) : super(key: key);
@override
State<HowToPlayDialog> createState() => _HowToPlayDialogState();
}
class _HowToPlayDialogState extends State<HowToPlayDialog> {
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final isMobile = widget.platformHelper.isMobile;
const spacing = SizedBox(height: 16); return PixelatedDecoration(
header: const _HowToPlayHeader(),
body: isMobile ? const _MobileBody() : const _DesktopBody(),
);
}
}
return Dialog( class _MobileBody extends StatelessWidget {
const _MobileBody({Key? key}) : super(key: key);
@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( child: Padding(
padding: const EdgeInsets.all(20), padding: EdgeInsets.symmetric(
horizontal: paddingWidth,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(l10n.howToPlay), const _MobileLaunchControls(),
SizedBox(height: paddingHeight),
const _MobileFlipperControls(),
],
),
),
);
}
}
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, spacing,
const _LaunchControls(), _DesktopLaunchControls(),
spacing, spacing,
const _FlipperControls(), _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 _LaunchControls extends StatelessWidget { class _DesktopLaunchControls extends StatelessWidget {
const _LaunchControls({Key? key}) : super(key: key); const _DesktopLaunchControls({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -39,17 +239,18 @@ class _LaunchControls extends StatelessWidget {
return Column( return Column(
children: [ children: [
Text(l10n.launchControls), Text(
l10n.launchControls,
style: AppTextStyle.headline4,
),
const SizedBox(height: 10), const SizedBox(height: 10),
Row( Wrap(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: const [
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down), KeyButton(control: Control.down),
spacing, spacing,
KeyIndicator.fromKeyName(keyName: 'SPACE'), KeyButton(control: Control.space),
spacing, spacing,
KeyIndicator.fromKeyName(keyName: 'S'), KeyButton(control: Control.s),
], ],
) )
], ],
@ -57,8 +258,8 @@ class _LaunchControls extends StatelessWidget {
} }
} }
class _FlipperControls extends StatelessWidget { class _DesktopFlipperControls extends StatelessWidget {
const _FlipperControls({Key? key}) : super(key: key); const _DesktopFlipperControls({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -67,7 +268,10 @@ class _FlipperControls extends StatelessWidget {
return Column( return Column(
children: [ children: [
Text(l10n.flipperControls), Text(
l10n.flipperControls,
style: AppTextStyle.subtitle2,
),
const SizedBox(height: 10), const SizedBox(height: 10),
Column( Column(
children: [ children: [
@ -75,19 +279,17 @@ class _FlipperControls extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: const [
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left), KeyButton(control: Control.left),
rowSpacing, rowSpacing,
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_right), KeyButton(control: Control.right),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Wrap(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: const [
KeyIndicator.fromKeyName(keyName: 'A'), KeyButton(control: Control.a),
rowSpacing, rowSpacing,
KeyIndicator.fromKeyName(keyName: 'D'), KeyButton(control: Control.d),
], ],
) )
], ],
@ -97,64 +299,45 @@ class _FlipperControls extends StatelessWidget {
} }
} }
// TODO(allisonryan0002): remove visibility when adding final UI.
@visibleForTesting @visibleForTesting
class KeyIndicator extends StatelessWidget { class KeyButton extends StatelessWidget {
const KeyIndicator._({ const KeyButton({
Key? key, Key? key,
required String keyName, required Control control,
required IconData keyIcon, }) : _control = control,
required bool fromIcon,
}) : _keyName = keyName,
_keyIcon = keyIcon,
_fromIcon = fromIcon,
super(key: key); super(key: key);
const KeyIndicator.fromKeyName({Key? key, required String keyName}) final Control _control;
: 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;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const iconPadding = EdgeInsets.all(15); final textStyle =
const textPadding = EdgeInsets.symmetric(vertical: 20, horizontal: 22); _control.isArrow ? AppTextStyle.headline1 : AppTextStyle.headline3;
final boarderColor = Colors.blue.withOpacity(0.5); const height = 60.0;
final color = Colors.blue.withOpacity(0.7); final width = _control.isSpace ? height * 2.83 : height;
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5), image: DecorationImage(
border: Border.all( fit: BoxFit.fill,
color: boarderColor, image: AssetImage(
width: 3, _control.isSpace
? Assets.images.components.space.keyName
: Assets.images.components.key.keyName,
),
),
),
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),
),
), ),
), ),
child: _fromIcon
? Padding(
padding: iconPadding,
child: Icon(_keyIcon, color: color),
)
: Padding(
padding: textPadding,
child: Text(_keyName, style: TextStyle(color: color)),
), ),
); );
} }

@ -7,7 +7,9 @@ abstract class AppColors {
static const Color darkBlue = Color(0xFF0C32A4); 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); static const Color blue = Color(0xFF4B94F6);

@ -27,6 +27,35 @@ abstract class AppTextStyle {
fontFamily: _primaryFontFamily, 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( static const subtitle1 = TextStyle(
fontSize: 10, fontSize: 10,
fontFamily: _primaryFontFamily, fontFamily: _primaryFontFamily,

@ -1,91 +1,6 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:leaderboard_repository/leaderboard_repository.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} /// {@template leaderboard_repository}
/// Repository to access leaderboard data in Firebase Cloud Firestore. /// Repository to access leaderboard data in Firebase Cloud Firestore.
/// {@endtemplate} /// {@endtemplate}
@ -97,73 +12,40 @@ class LeaderboardRepository {
final FirebaseFirestore _firebaseFirestore; final FirebaseFirestore _firebaseFirestore;
static const _leaderboardLimit = 10;
static const _leaderboardCollectionName = 'leaderboard';
static const _scoreFieldName = 'score';
/// Acquires top 10 [LeaderboardEntryData]s. /// Acquires top 10 [LeaderboardEntryData]s.
Future<List<LeaderboardEntryData>> fetchTop10Leaderboard() async { Future<List<LeaderboardEntryData>> fetchTop10Leaderboard() async {
final leaderboardEntries = <LeaderboardEntryData>[];
late List<QueryDocumentSnapshot> documents;
try { try {
final querySnapshot = await _firebaseFirestore final querySnapshot = await _firebaseFirestore
.collection('leaderboard') .collection(_leaderboardCollectionName)
.orderBy('score', descending: true) .orderBy(_scoreFieldName, descending: true)
.limit(10) .limit(_leaderboardLimit)
.get(); .get();
documents = querySnapshot.docs; final documents = querySnapshot.docs;
return documents.toLeaderboard();
} on LeaderboardDeserializationException {
rethrow;
} on Exception catch (error, stackTrace) { } on Exception catch (error, stackTrace) {
throw FetchTop10LeaderboardException(error, stackTrace); throw FetchTop10LeaderboardException(error, stackTrace);
} }
for (final document in documents) {
final data = document.data() as Map<String, dynamic>?;
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 if it is within the top-10
} Future<void> addLeaderboardEntry(
/// Adds player's score entry to the leaderboard and gets their
/// [LeaderboardRanking].
Future<LeaderboardRanking> addLeaderboardEntry(
LeaderboardEntryData entry, LeaderboardEntryData entry,
) async { ) async {
late DocumentReference entryReference; final leaderboard = await _fetchLeaderboardSortedByScore();
try { if (leaderboard.length < 10) {
entryReference = await _firebaseFirestore await _saveScore(entry);
.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 { } else {
throw FetchPlayerRankingException( final tenthPositionScore = leaderboard[9].score;
'Player score could not be found and ranking cannot be provided.', if (entry.score > tenthPositionScore) {
StackTrace.current, 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)) { if (!initialsRegex.hasMatch(initials)) {
return false; return false;
} }
try { try {
final document = await _firebaseFirestore final document = await _firebaseFirestore
.collection('prohibitedInitials') .collection('prohibitedInitials')
@ -187,4 +68,61 @@ class LeaderboardRepository {
throw FetchProhibitedInitialsException(error, stackTrace); throw FetchProhibitedInitialsException(error, stackTrace);
} }
} }
Future<List<LeaderboardEntryData>> _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<void> _saveScore(LeaderboardEntryData entry) {
try {
return _firebaseFirestore
.collection(_leaderboardCollectionName)
.add(entry.toJson());
} on Exception catch (error, stackTrace) {
throw AddLeaderboardEntryException(error, stackTrace);
}
}
Future<void> _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<QueryDocumentSnapshot> {
List<LeaderboardEntryData> toLeaderboard() {
final leaderboardEntries = <LeaderboardEntryData>[];
for (final document in this) {
final data = document.data() as Map<String, dynamic>?;
if (data != null) {
try {
leaderboardEntries.add(LeaderboardEntryData.fromJson(data));
} catch (error, stackTrace) {
throw LeaderboardDeserializationException(error, stackTrace);
}
}
}
return leaderboardEntries;
}
} }

@ -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);
}

@ -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<Object> get props => [ranking, outOf];
}

@ -1,2 +1,2 @@
export 'exceptions.dart';
export 'leaderboard_entry_data.dart'; export 'leaderboard_entry_data.dart';
export 'leaderboard_ranking.dart';

@ -153,7 +153,6 @@ void main() {
character: CharacterType.dash, character: CharacterType.dash,
); );
const entryDocumentId = 'id$entryScore'; const entryDocumentId = 'id$entryScore';
final ranking = LeaderboardRanking(ranking: 3, outOf: 4);
setUp(() { setUp(() {
leaderboardRepository = LeaderboardRepository(firestore); leaderboardRepository = LeaderboardRepository(firestore);
@ -165,13 +164,12 @@ void main() {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'username': 'user$score', 'playerInitials': 'AAA',
'score': score 'score': score
}); });
when(() => queryDocumentSnapshot.id).thenReturn('id$score'); when(() => queryDocumentSnapshot.id).thenReturn('id$score');
return queryDocumentSnapshot; return queryDocumentSnapshot;
}).toList(); }).toList();
when(() => firestore.collection('leaderboard')) when(() => firestore.collection('leaderboard'))
.thenAnswer((_) => collectionReference); .thenAnswer((_) => collectionReference);
when(() => collectionReference.add(any())) when(() => collectionReference.add(any()))
@ -184,19 +182,29 @@ void main() {
}); });
test( test(
'adds leaderboard entry and returns player ranking when ' 'throws FetchLeaderboardException '
'firestore operations succeed', () async { 'when querying the leaderboard fails', () {
final rankingResult = when(() => firestore.collection('leaderboard')).thenThrow(Exception());
await leaderboardRepository.addLeaderboardEntry(leaderboardEntry); expect(
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry),
expect(rankingResult, equals(ranking)); throwsA(isA<FetchLeaderboardException>()),
);
}); });
test( test(
'throws AddLeaderboardEntryException when Exception occurs ' 'saves the new score if the existing leaderboard '
'when trying to add entry to firestore', () async { 'has less than 10 scores', () async {
when(() => firestore.collection('leaderboard')).thenThrow(Exception()); 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( expect(
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry),
throwsA(isA<AddLeaderboardEntryException>()), throwsA(isA<AddLeaderboardEntryException>()),
@ -204,26 +212,160 @@ void main() {
}); });
test( test(
'throws FetchPlayerRankingException when Exception occurs ' 'does nothing if there are more than 10 scores in the leaderboard '
'when trying to retrieve information from firestore', () async { 'and the new score is smaller than the top 10', () async {
when(() => collectionReference.orderBy('score', descending: true)) final leaderboardScores = [
.thenThrow(Exception()); 10000,
9500,
9000,
8500,
8000,
7500,
7000,
6500,
6000,
5500,
5000
];
final queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash',
'playerInitials': 'AAA',
'score': score
});
when(() => queryDocumentSnapshot.id).thenReturn('id$score');
return queryDocumentSnapshot;
}).toList();
when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots);
expect( await leaderboardRepository.addLeaderboardEntry(leaderboardEntry);
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), verifyNever(
throwsA(isA<FetchPlayerRankingException>()), () => collectionReference.add(leaderboardEntry.toJson()),
); );
}); });
test( test(
'throws FetchPlayerRankingException when score cannot be found ' 'throws DeleteLeaderboardException '
'in firestore leaderboard data', () async { 'when deleting scores outside the top 10 fails', () async {
when(() => documentReference.id).thenReturn('nonexistentDocumentId'); 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(<String, dynamic>{
'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(<String, dynamic>{
'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( expect(
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), () => leaderboardRepository.addLeaderboardEntry(newScore),
throwsA(isA<FetchPlayerRankingException>()), throwsA(isA<DeleteLeaderboardException>()),
);
});
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(<String, dynamic>{
'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(<String, dynamic>{
'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);
}); });
}); });

@ -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));
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

@ -10,8 +10,7 @@ import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
$AssetsImagesAndroidBumperGen get androidBumper => $AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen();
const $AssetsImagesAndroidBumperGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
@ -24,20 +23,23 @@ class $AssetsImagesGen {
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp => $AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
$AssetsImagesMultiplierGen get multiplier =>
const $AssetsImagesMultiplierGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
} }
class $AssetsImagesAndroidBumperGen { class $AssetsImagesAndroidGen {
const $AssetsImagesAndroidBumperGen(); const $AssetsImagesAndroidGen();
$AssetsImagesAndroidBumperAGen get a => $AssetsImagesAndroidBumperGen get bumper =>
const $AssetsImagesAndroidBumperAGen(); const $AssetsImagesAndroidBumperGen();
$AssetsImagesAndroidBumperBGen get b => $AssetsImagesAndroidRailGen get rail => const $AssetsImagesAndroidRailGen();
const $AssetsImagesAndroidBumperBGen(); $AssetsImagesAndroidRampGen get ramp => const $AssetsImagesAndroidRampGen();
$AssetsImagesAndroidSpaceshipGen get spaceship =>
const $AssetsImagesAndroidSpaceshipGen();
} }
class $AssetsImagesBackboardGen { class $AssetsImagesBackboardGen {
@ -188,6 +190,16 @@ class $AssetsImagesLaunchRampGen {
const AssetGenImage('assets/images/launch_ramp/ramp.png'); 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 { class $AssetsImagesPlungerGen {
const $AssetsImagesPlungerGen(); const $AssetsImagesPlungerGen();
@ -232,56 +244,79 @@ class $AssetsImagesSlingshotGen {
const AssetGenImage('assets/images/slingshot/upper.png'); 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 { class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyGen(); const $AssetsImagesSparkyGen();
/// File path: assets/images/sparky/animatronic.png
AssetGenImage get animatronic => AssetGenImage get animatronic =>
const AssetGenImage('assets/images/sparky/animatronic.png'); const AssetGenImage('assets/images/sparky/animatronic.png');
$AssetsImagesSparkyBumperGen get bumper => $AssetsImagesSparkyBumperGen get bumper =>
const $AssetsImagesSparkyBumperGen(); const $AssetsImagesSparkyBumperGen();
$AssetsImagesSparkyComputerGen get computer => $AssetsImagesSparkyComputerGen get computer =>
const $AssetsImagesSparkyComputerGen(); const $AssetsImagesSparkyComputerGen();
} }
class $AssetsImagesAndroidBumperAGen { class $AssetsImagesAndroidBumperGen {
const $AssetsImagesAndroidBumperGen();
$AssetsImagesAndroidBumperAGen get a =>
const $AssetsImagesAndroidBumperAGen(); const $AssetsImagesAndroidBumperAGen();
$AssetsImagesAndroidBumperBGen get b =>
const $AssetsImagesAndroidBumperBGen();
$AssetsImagesAndroidBumperCowGen get cow =>
const $AssetsImagesAndroidBumperCowGen();
}
/// File path: assets/images/android_bumper/a/dimmed.png class $AssetsImagesAndroidRailGen {
AssetGenImage get dimmed => const $AssetsImagesAndroidRailGen();
const AssetGenImage('assets/images/android_bumper/a/dimmed.png');
/// File path: assets/images/android_bumper/a/lit.png /// File path: assets/images/android/rail/exit.png
AssetGenImage get lit => AssetGenImage get exit =>
const AssetGenImage('assets/images/android_bumper/a/lit.png'); 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 { class $AssetsImagesAndroidRampGen {
const $AssetsImagesAndroidBumperBGen(); const $AssetsImagesAndroidRampGen();
/// File path: assets/images/android_bumper/b/dimmed.png $AssetsImagesAndroidRampArrowGen get arrow =>
AssetGenImage get dimmed => const $AssetsImagesAndroidRampArrowGen();
const AssetGenImage('assets/images/android_bumper/b/dimmed.png');
/// File path: assets/images/android_bumper/b/lit.png /// File path: assets/images/android/ramp/board-opening.png
AssetGenImage get lit => AssetGenImage get boardOpening =>
const AssetGenImage('assets/images/android_bumper/b/lit.png'); 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 { class $AssetsImagesDashBumperGen {
@ -305,39 +340,64 @@ class $AssetsImagesDinoAnimatronicGen {
const AssetGenImage('assets/images/dino/animatronic/mouth.png'); const AssetGenImage('assets/images/dino/animatronic/mouth.png');
} }
class $AssetsImagesSpaceshipRailGen { class $AssetsImagesMultiplierX2Gen {
const $AssetsImagesSpaceshipRailGen(); const $AssetsImagesMultiplierX2Gen();
/// File path: assets/images/spaceship/rail/exit.png /// File path: assets/images/multiplier/x2/dimmed.png
AssetGenImage get exit => AssetGenImage get dimmed =>
const AssetGenImage('assets/images/spaceship/rail/exit.png'); const AssetGenImage('assets/images/multiplier/x2/dimmed.png');
/// File path: assets/images/spaceship/rail/main.png /// File path: assets/images/multiplier/x2/lit.png
AssetGenImage get main => AssetGenImage get lit =>
const AssetGenImage('assets/images/spaceship/rail/main.png'); const AssetGenImage('assets/images/multiplier/x2/lit.png');
} }
class $AssetsImagesSpaceshipRampGen { class $AssetsImagesMultiplierX3Gen {
const $AssetsImagesSpaceshipRampGen(); const $AssetsImagesMultiplierX3Gen();
$AssetsImagesSpaceshipRampArrowGen get arrow => /// File path: assets/images/multiplier/x3/dimmed.png
const $AssetsImagesSpaceshipRampArrowGen(); AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiplier/x3/dimmed.png');
/// File path: assets/images/spaceship/ramp/board-opening.png /// File path: assets/images/multiplier/x3/lit.png
AssetGenImage get boardOpening => AssetGenImage get lit =>
const AssetGenImage('assets/images/spaceship/ramp/board-opening.png'); const AssetGenImage('assets/images/multiplier/x3/lit.png');
}
/// File path: assets/images/spaceship/ramp/main.png class $AssetsImagesMultiplierX4Gen {
AssetGenImage get main => const $AssetsImagesMultiplierX4Gen();
const AssetGenImage('assets/images/spaceship/ramp/main.png');
/// 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/spaceship/ramp/railing-background.png /// File path: assets/images/multiplier/x5/dimmed.png
AssetGenImage get railingBackground => const AssetGenImage( AssetGenImage get dimmed =>
'assets/images/spaceship/ramp/railing-background.png'); const AssetGenImage('assets/images/multiplier/x5/dimmed.png');
/// File path: assets/images/spaceship/ramp/railing-foreground.png /// File path: assets/images/multiplier/x5/lit.png
AssetGenImage get railingForeground => const AssetGenImage( AssetGenImage get lit =>
'assets/images/spaceship/ramp/railing-foreground.png'); const AssetGenImage('assets/images/multiplier/x5/lit.png');
}
class $AssetsImagesMultiplierX6Gen {
const $AssetsImagesMultiplierX6Gen();
/// 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 { class $AssetsImagesSparkyBumperGen {
@ -360,6 +420,70 @@ class $AssetsImagesSparkyComputerGen {
const AssetGenImage('assets/images/sparky/computer/top.png'); 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 { class $AssetsImagesDashBumperAGen {
const $AssetsImagesDashBumperAGen(); const $AssetsImagesDashBumperAGen();
@ -396,34 +520,6 @@ class $AssetsImagesDashBumperMainGen {
const AssetGenImage('assets/images/dash/bumper/main/inactive.png'); 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 { class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen(); const $AssetsImagesSparkyBumperAGen();

@ -10,7 +10,7 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_bumper_cubit.dart'; export 'cubit/android_bumper_cubit.dart';
/// {@template android_bumper} /// {@template android_bumper}
/// Bumper for area under the [Spaceship]. /// Bumper for area under the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class AndroidBumper extends BodyComponent with InitialPosition { class AndroidBumper extends BodyComponent with InitialPosition {
/// {@macro android_bumper} /// {@macro android_bumper}
@ -19,6 +19,7 @@ class AndroidBumper extends BodyComponent with InitialPosition {
required double minorRadius, required double minorRadius,
required String litAssetPath, required String litAssetPath,
required String dimmedAssetPath, required String dimmedAssetPath,
required Vector2 spritePosition,
Iterable<Component>? children, Iterable<Component>? children,
required this.bloc, required this.bloc,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
@ -32,6 +33,7 @@ class AndroidBumper extends BodyComponent with InitialPosition {
_AndroidBumperSpriteGroupComponent( _AndroidBumperSpriteGroupComponent(
dimmedAssetPath: dimmedAssetPath, dimmedAssetPath: dimmedAssetPath,
litAssetPath: litAssetPath, litAssetPath: litAssetPath,
position: spritePosition,
state: bloc.state, state: bloc.state,
), ),
...?children, ...?children,
@ -44,8 +46,9 @@ class AndroidBumper extends BodyComponent with InitialPosition {
}) : this._( }) : this._(
majorRadius: 3.52, majorRadius: 3.52,
minorRadius: 2.97, minorRadius: 2.97,
litAssetPath: Assets.images.androidBumper.a.lit.keyName, litAssetPath: Assets.images.android.bumper.a.lit.keyName,
dimmedAssetPath: Assets.images.androidBumper.a.dimmed.keyName, dimmedAssetPath: Assets.images.android.bumper.a.dimmed.keyName,
spritePosition: Vector2(0, -0.1),
bloc: AndroidBumperCubit(), bloc: AndroidBumperCubit(),
children: children, children: children,
); );
@ -56,8 +59,22 @@ class AndroidBumper extends BodyComponent with InitialPosition {
}) : this._( }) : this._(
majorRadius: 3.19, majorRadius: 3.19,
minorRadius: 2.79, minorRadius: 2.79,
litAssetPath: Assets.images.androidBumper.b.lit.keyName, litAssetPath: Assets.images.android.bumper.b.lit.keyName,
dimmedAssetPath: Assets.images.androidBumper.b.dimmed.keyName, dimmedAssetPath: Assets.images.android.bumper.b.dimmed.keyName,
spritePosition: Vector2(0, -0.1),
bloc: AndroidBumperCubit(),
children: children,
);
/// {@macro android_bumper}
AndroidBumper.cow({
Iterable<Component>? 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(), bloc: AndroidBumperCubit(),
children: children, children: children,
); );
@ -113,12 +130,13 @@ class _AndroidBumperSpriteGroupComponent
_AndroidBumperSpriteGroupComponent({ _AndroidBumperSpriteGroupComponent({
required String litAssetPath, required String litAssetPath,
required String dimmedAssetPath, required String dimmedAssetPath,
required Vector2 position,
required AndroidBumperState state, required AndroidBumperState state,
}) : _litAssetPath = litAssetPath, }) : _litAssetPath = litAssetPath,
_dimmedAssetPath = dimmedAssetPath, _dimmedAssetPath = dimmedAssetPath,
super( super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -0.1), position: position,
current: state, current: state,
); );

@ -5,7 +5,7 @@ import 'package:bloc/bloc.dart';
part 'android_bumper_state.dart'; part 'android_bumper_state.dart';
class AndroidBumperCubit extends Cubit<AndroidBumperState> { class AndroidBumperCubit extends Cubit<AndroidBumperState> {
AndroidBumperCubit() : super(AndroidBumperState.dimmed); AndroidBumperCubit() : super(AndroidBumperState.lit);
void onBallContacted() { void onBallContacted() {
emit(AndroidBumperState.dimmed); emit(AndroidBumperState.dimmed);

@ -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<void> 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<void> 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<void> 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,
);
}
}

@ -1,4 +1,5 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template bumping_behavior} /// {@template bumping_behavior}
@ -11,15 +12,22 @@ class BumpingBehavior extends ContactBehavior {
/// Determines how strong the bump is. /// Determines how strong the bump is.
final double _strength; final double _strength;
/// This is used to recoginze the current state of a contact manifold in world
/// coordinates.
@visibleForTesting
final WorldManifold worldManifold = WorldManifold();
@override @override
void postSolve(Object other, Contact contact, ContactImpulse impulse) { void postSolve(Object other, Contact contact, ContactImpulse impulse) {
super.postSolve(other, contact, impulse); super.postSolve(other, contact, impulse);
if (other is! BodyComponent) return; if (other is! BodyComponent) return;
contact.getWorldManifold(worldManifold);
other.body.applyLinearImpulse( other.body.applyLinearImpulse(
contact.manifold.localPoint worldManifold.normal
..normalize() ..multiply(
..multiply(Vector2.all(other.body.mass * _strength)), Vector2.all(other.body.mass * _strength),
),
); );
} }
} }

@ -1,4 +1,5 @@
export 'android_bumper/android_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'android_spaceship.dart';
export 'backboard/backboard.dart'; export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
@ -19,6 +20,7 @@ export 'kicker.dart';
export 'launch_ramp.dart'; export 'launch_ramp.dart';
export 'layer.dart'; export 'layer.dart';
export 'layer_sensor.dart'; export 'layer_sensor.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'render_priority.dart'; export 'render_priority.dart';
export 'rocket.dart'; export 'rocket.dart';
@ -26,7 +28,6 @@ export 'score_text.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'signpost.dart'; export 'signpost.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export 'spaceship_ramp.dart'; export 'spaceship_ramp.dart';
export 'sparky_animatronic.dart'; export 'sparky_animatronic.dart';

@ -55,7 +55,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
); );
final joint = _FlipperJoint(jointDef); final joint = _FlipperJoint(jointDef);
world.createJoint(joint); world.createJoint(joint);
unawaited(mounted.whenComplete(joint.unlock));
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
@ -132,6 +131,15 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
return body; return body;
} }
@override
void onMount() {
super.onMount();
gameRef.ready().whenComplete(
() => body.joints.whereType<_FlipperJoint>().first.unlock(),
);
}
} }
class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { 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] /// The joint is locked when initialized in order to force the [Flipper]
/// at its resting position. /// at its resting position.
void lock() { void lock() {
const angle = _halfSweepingAngle; final angle = _halfSweepingAngle * side.direction;
setLimits( setLimits(angle, angle);
angle * side.direction,
angle * side.direction,
);
} }
/// Unlocks the [Flipper] from its resting position. /// Unlocks the [Flipper] from its resting position.

@ -13,12 +13,14 @@ export 'cubit/google_letter_cubit.dart';
class GoogleLetter extends BodyComponent with InitialPosition { class GoogleLetter extends BodyComponent with InitialPosition {
/// {@macro google_letter} /// {@macro google_letter}
GoogleLetter( GoogleLetter(
int index, int index, {
) : bloc = GoogleLetterCubit(), Iterable<Component>? children,
}) : bloc = GoogleLetterCubit(),
super( super(
children: [ children: [
GoogleLetterBallContactBehavior(), GoogleLetterBallContactBehavior(),
_GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index]) _GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index]),
...?children,
], ],
); );

@ -16,9 +16,13 @@ class Kicker extends BodyComponent with InitialPosition {
/// {@macro kicker} /// {@macro kicker}
Kicker({ Kicker({
required BoardSide side, required BoardSide side,
Iterable<Component>? children,
}) : _side = side, }) : _side = side,
super( super(
children: [_KickerSpriteComponent(side: side)], children: [
_KickerSpriteComponent(side: side),
...?children,
],
renderBody: false, renderBody: false,
); );

@ -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<MultiplierState> {
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));
}
}
}
}

@ -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<Object> 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;
}
}
}

@ -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<void> 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<MultiplierSpriteState>
with HasGameRef, ParentIsA<Multiplier> {
/// {@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<void> 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;
}
}

@ -82,7 +82,7 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
/// The velocity's magnitude depends on how far the [Plunger] has been pulled /// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition]. /// from its original [initialPosition].
void release() { void release() {
final velocity = (initialPosition.y - body.position.y) * 5; final velocity = (initialPosition.y - body.position.y) * 7;
body.linearVelocity = Vector2(0, velocity); body.linearVelocity = Vector2(0, velocity);
_spriteComponent.release(); _spriteComponent.release();
} }
@ -221,7 +221,7 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
plunger.body, plunger.body,
anchor.body, anchor.body,
plunger.body.position + anchor.body.position, plunger.body.position + anchor.body.position,
Vector2(18.6, BoardDimensions.bounds.height), Vector2(16, BoardDimensions.bounds.height),
); );
enableLimit = true; enableLimit = true;
lowerTranslation = double.negativeInfinity; lowerTranslation = double.negativeInfinity;

@ -20,14 +20,14 @@ abstract class RenderPriority {
static const int ballOnSpaceshipRamp = static const int ballOnSpaceshipRamp =
_above + spaceshipRampBackgroundRailing; _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; static const int ballOnSpaceship = _above + spaceshipSaucer;
/// Render priority for the [Ball] while it's on the [SpaceshipRail]. /// Render priority for the [Ball] while it's on the [SpaceshipRail].
static const int ballOnSpaceshipRail = _above + spaceshipRail; static const int ballOnSpaceshipRail = _above + spaceshipRail;
/// Render priority for the [Ball] while it's on the [LaunchRamp]. /// Render priority for the [Ball] while it's on the [LaunchRamp].
static const int ballOnLaunchRamp = _above + launchRamp; static const int ballOnLaunchRamp = launchRamp;
// Background // Background
@ -51,11 +51,11 @@ abstract class RenderPriority {
static const int launchRamp = _above + outerBoundary; 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 plunger = _above + launchRamp;
static const int rocket = _above + bottomBoundary; static const int rocket = _below + bottomBoundary;
// Dino Land // Dino Land
@ -91,7 +91,7 @@ abstract class RenderPriority {
static const int spaceshipSaucer = _above + ballOnSpaceshipRail; static const int spaceshipSaucer = _above + ballOnSpaceshipRail;
static const int spaceshipSaucerWall = _above + spaceshipSaucer; static const int spaceshipLightBeam = _below + spaceshipSaucer;
static const int androidHead = _above + spaceshipSaucer; static const int androidHead = _above + spaceshipSaucer;

@ -6,19 +6,22 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
/// A [SpriteComponent] for the rocket over [Plunger]. /// A [SpriteComponent] for the rocket over [Plunger].
/// {@endtemplate} /// {@endtemplate}
class RocketSpriteComponent extends SpriteComponent with HasGameRef { class RocketSpriteComponent extends SpriteComponent with HasGameRef {
// TODO(ruimiguel): change this priority to be over launcher ramp and bottom
// wall.
/// {@macro rocket_sprite_component} /// {@macro rocket_sprite_component}
RocketSpriteComponent() : super(priority: RenderPriority.rocket); RocketSpriteComponent()
: super(
priority: RenderPriority.rocket,
anchor: Anchor.center,
);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprite = await gameRef.loadSprite( final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.plunger.rocket.keyName, Assets.images.plunger.rocket.keyName,
),
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center;
} }
} }

@ -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<void> 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<void> 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);
}
}

@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_rail} /// {@template spaceship_rail}
/// A [Blueprint] for the rail exiting the [Spaceship]. /// A [Blueprint] for the rail exiting the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRail extends Blueprint { class SpaceshipRail extends Blueprint {
/// {@macro spaceship_rail} /// {@macro spaceship_rail}
@ -116,7 +116,7 @@ class _SpaceshipRailSpriteComponent extends SpriteComponent with HasGameRef {
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.rail.main.keyName, Assets.images.android.rail.main.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -139,7 +139,7 @@ class _SpaceshipRailExitSpriteComponent extends SpriteComponent
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.rail.exit.keyName, Assets.images.android.rail.exit.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;

@ -8,7 +8,7 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_ramp} /// {@template spaceship_ramp}
/// A [Blueprint] which creates the ramp leading into the [Spaceship]. /// A [Blueprint] which creates the ramp leading into the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRamp extends Blueprint { class SpaceshipRamp extends Blueprint {
/// {@macro spaceship_ramp} /// {@macro spaceship_ramp}
@ -73,17 +73,17 @@ extension on SpaceshipRampArrowSpriteState {
String get path { String get path {
switch (this) { switch (this) {
case SpaceshipRampArrowSpriteState.inactive: case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.spaceship.ramp.arrow.inactive.keyName; return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1: case SpaceshipRampArrowSpriteState.active1:
return Assets.images.spaceship.ramp.arrow.active1.keyName; return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2: case SpaceshipRampArrowSpriteState.active2:
return Assets.images.spaceship.ramp.arrow.active2.keyName; return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3: case SpaceshipRampArrowSpriteState.active3:
return Assets.images.spaceship.ramp.arrow.active3.keyName; return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4: case SpaceshipRampArrowSpriteState.active4:
return Assets.images.spaceship.ramp.arrow.active4.keyName; return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5: 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(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.android.ramp.railingBackground.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -182,7 +182,7 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.main.keyName, Assets.images.android.ramp.main.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -234,7 +234,7 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.android.ramp.boardOpening.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -304,7 +304,7 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.android.ramp.railingForeground.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;

@ -45,7 +45,6 @@ flutter:
- asset: fonts/PixeloidMono-1G8ae.ttf - asset: fonts/PixeloidMono-1G8ae.ttf
assets: assets:
- assets/images/
- assets/images/ball/ - assets/images/ball/
- assets/images/baseboard/ - assets/images/baseboard/
- assets/images/boundary/ - assets/images/boundary/
@ -57,15 +56,16 @@ flutter:
- assets/images/dash/bumper/a/ - assets/images/dash/bumper/a/
- assets/images/dash/bumper/b/ - assets/images/dash/bumper/b/
- assets/images/dash/bumper/main/ - assets/images/dash/bumper/main/
- assets/images/spaceship/ - assets/images/android/spaceship/
- assets/images/spaceship/rail/ - assets/images/android/rail/
- assets/images/spaceship/ramp/ - assets/images/android/ramp/
- assets/images/spaceship/ramp/arrow/ - 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/kicker/
- assets/images/plunger/ - assets/images/plunger/
- assets/images/slingshot/ - assets/images/slingshot/
- assets/images/android_bumper/a/
- assets/images/android_bumper/b/
- assets/images/sparky/ - assets/images/sparky/
- assets/images/sparky/computer/ - assets/images/sparky/computer/
- assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/a/
@ -74,6 +74,11 @@ flutter:
- assets/images/backboard/ - assets/images/backboard/
- assets/images/google_word/ - assets/images/google_word/
- assets/images/signpost/ - 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: flutter_gen:
line_length: 80 line_length: 80

@ -6,7 +6,6 @@
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
import 'package:dashbook/dashbook.dart'; import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sandbox/stories/kicker/stories.dart';
import 'package:sandbox/stories/stories.dart'; import 'package:sandbox/stories/stories.dart';
void main() { void main() {
@ -15,11 +14,9 @@ void main() {
addBallStories(dashbook); addBallStories(dashbook);
addLayerStories(dashbook); addLayerStories(dashbook);
addEffectsStories(dashbook); addEffectsStories(dashbook);
addFlipperStories(dashbook);
addBaseboardStories(dashbook);
addChromeDinoStories(dashbook); addChromeDinoStories(dashbook);
addDashNestBumperStories(dashbook); addFlutterForestStories(dashbook);
addKickerStories(dashbook); addBottomGroupStories(dashbook);
addPlungerStories(dashbook); addPlungerStories(dashbook);
addSlingshotStories(dashbook); addSlingshotStories(dashbook);
addSparkyBumperStories(dashbook); addSparkyBumperStories(dashbook);
@ -30,6 +27,7 @@ void main() {
addScoreTextStories(dashbook); addScoreTextStories(dashbook);
addBackboardStories(dashbook); addBackboardStories(dashbook);
addDinoWallStories(dashbook); addDinoWallStories(dashbook);
addMultipliersStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -9,8 +9,8 @@ class AndroidBumperAGame extends BallGame {
: super( : super(
color: const Color(0xFF0000FF), color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.androidBumper.a.lit.keyName, Assets.images.android.bumper.a.lit.keyName,
Assets.images.androidBumper.a.dimmed.keyName, Assets.images.android.bumper.a.dimmed.keyName,
], ],
); );

@ -9,8 +9,8 @@ class AndroidBumperBGame extends BallGame {
: super( : super(
color: const Color(0xFF0000FF), color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.androidBumper.b.lit.keyName, Assets.images.android.bumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,
], ],
); );

@ -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<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(
AndroidBumper.cow()..priority = 1,
);
await traceAllBodies();
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save