@ -0,0 +1,22 @@
|
|||||||
|
name: authentication_repository
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "packages/authentication_repository/**"
|
||||||
|
- ".github/workflows/authentication_repository.yaml"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "packages/authentication_repository/**"
|
||||||
|
- ".github/workflows/authentication_repository.yaml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
|
||||||
|
with:
|
||||||
|
working_directory: packages/authentication_repository
|
@ -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"
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 306 KiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
/// {@template footer}
|
||||||
|
/// Footer widget with links to the main tech stack.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Footer extends StatelessWidget {
|
||||||
|
/// {@macro footer}
|
||||||
|
const Footer({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(50, 0, 50, 32),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: const [
|
||||||
|
_MadeWithFlutterAndFirebase(),
|
||||||
|
_GoogleIO(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GoogleIO extends StatelessWidget {
|
||||||
|
const _GoogleIO({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Text(
|
||||||
|
l10n.footerGoogleIOText,
|
||||||
|
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MadeWithFlutterAndFirebase extends StatelessWidget {
|
||||||
|
const _MadeWithFlutterAndFirebase({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return RichText(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
text: TextSpan(
|
||||||
|
text: l10n.footerMadeWithText,
|
||||||
|
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: l10n.footerFlutterLinkText,
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => openLink('https://flutter.dev'),
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: ' & '),
|
||||||
|
TextSpan(
|
||||||
|
text: l10n.footerFirebaseLinkText,
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => openLink('https://firebase.google.com'),
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
// ignore_for_file: avoid_renaming_method_parameters
|
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
|
|
||||||
/// {@template alien_zone}
|
|
||||||
/// Area positioned below [Spaceship] where the [Ball]
|
|
||||||
/// can bounce off [AlienBumper]s.
|
|
||||||
///
|
|
||||||
/// When a [Ball] hits an [AlienBumper], the bumper animates.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class AlienZone extends Component with HasGameRef<PinballGame> {
|
|
||||||
/// {@macro alien_zone}
|
|
||||||
AlienZone();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
|
|
||||||
gameRef.addContactCallback(AlienBumperBallContactCallback());
|
|
||||||
|
|
||||||
final lowerBumper = _AlienBumper.a()
|
|
||||||
..initialPosition = Vector2(-32.52, -9.1);
|
|
||||||
final upperBumper = _AlienBumper.b()
|
|
||||||
..initialPosition = Vector2(-22.89, -17.35);
|
|
||||||
|
|
||||||
await addAll([
|
|
||||||
lowerBumper,
|
|
||||||
upperBumper,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
|
|
||||||
// ContactCallback process is enhanced.
|
|
||||||
class _AlienBumper extends AlienBumper with ScorePoints {
|
|
||||||
_AlienBumper.a() : super.a();
|
|
||||||
|
|
||||||
_AlienBumper.b() : super.b();
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get points => 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Listens when a [Ball] bounces against an [AlienBumper].
|
|
||||||
@visibleForTesting
|
|
||||||
class AlienBumperBallContactCallback
|
|
||||||
extends ContactCallback<AlienBumper, Ball> {
|
|
||||||
@override
|
|
||||||
void begin(
|
|
||||||
AlienBumper alienBumper,
|
|
||||||
Ball _,
|
|
||||||
Contact __,
|
|
||||||
) {
|
|
||||||
alienBumper.animate();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,36 @@
|
|||||||
|
// ignore_for_file: avoid_renaming_method_parameters
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template android_acres}
|
||||||
|
/// Area positioned on the left side of the board containing the
|
||||||
|
/// [AndroidSpaceship], [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class AndroidAcres extends Component {
|
||||||
|
/// {@macro android_acres}
|
||||||
|
AndroidAcres()
|
||||||
|
: super(
|
||||||
|
children: [
|
||||||
|
SpaceshipRamp(),
|
||||||
|
SpaceshipRail(),
|
||||||
|
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
|
||||||
|
AndroidBumper.a(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 20000),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(-25, 1.3),
|
||||||
|
AndroidBumper.b(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 20000),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(-32.8, -9.2),
|
||||||
|
AndroidBumper.cow(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 20),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(-20.5, -13.8),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
@ -1,95 +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();
|
|
||||||
|
|
||||||
// TODO(alestiago): adjust positioning to real design.
|
|
||||||
// TODO(alestiago): add dino in pinball game.
|
|
||||||
final dino = ChromeDino()
|
|
||||||
..initialPosition = Vector2(
|
|
||||||
BoardDimensions.bounds.center.dx + 25,
|
|
||||||
BoardDimensions.bounds.center.dy - 10,
|
|
||||||
);
|
|
||||||
|
|
||||||
await addAll([
|
|
||||||
bottomGroup,
|
|
||||||
dino,
|
|
||||||
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,63 @@
|
|||||||
|
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 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 with ZIndex {
|
||||||
|
/// {@macro bottom_group}
|
||||||
|
BottomGroup()
|
||||||
|
: super(
|
||||||
|
children: [
|
||||||
|
_BottomGroupSide(side: BoardSide.right),
|
||||||
|
_BottomGroupSide(side: BoardSide.left),
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
zIndex = ZIndexes.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.66;
|
||||||
|
|
||||||
|
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.71,
|
||||||
|
);
|
||||||
|
final kicker = Kicker(
|
||||||
|
side: _side,
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 5000)..applyTo(['bouncy_edge']),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(
|
||||||
|
(22.64 * direction) + centerXAdjustment,
|
||||||
|
25.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await addAll([flipper, baseboard, kicker]);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,15 @@
|
|||||||
export 'alien_zone.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 'flutter_forest.dart';
|
export 'dino_desert.dart';
|
||||||
|
export 'drain.dart';
|
||||||
|
export 'flutter_forest/flutter_forest.dart';
|
||||||
export 'game_flow_controller.dart';
|
export 'game_flow_controller.dart';
|
||||||
export 'google_word.dart';
|
export 'google_word/google_word.dart';
|
||||||
export 'launcher.dart';
|
export 'launcher.dart';
|
||||||
export 'score_points.dart';
|
export 'multipliers/multipliers.dart';
|
||||||
export 'sparky_fire_zone.dart';
|
export 'scoring_behavior.dart';
|
||||||
export 'wall.dart';
|
export 'sparky_scorch.dart';
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.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 Component {
|
||||||
|
/// {@macro dino_desert}
|
||||||
|
DinoDesert()
|
||||||
|
: super(
|
||||||
|
children: [
|
||||||
|
ChromeDino()..initialPosition = Vector2(12.3, -6.9),
|
||||||
|
DinoWalls(),
|
||||||
|
Slingshots(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,102 +0,0 @@
|
|||||||
// ignore_for_file: avoid_renaming_method_parameters
|
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// {@template flutter_forest}
|
|
||||||
/// Area positioned at the top right of the [Board] where the [Ball]
|
|
||||||
/// can bounce off [DashNestBumper]s.
|
|
||||||
///
|
|
||||||
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
|
|
||||||
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
|
|
||||||
/// {@endtemplate}
|
|
||||||
class FlutterForest extends Component
|
|
||||||
with Controls<_FlutterForestController>, HasGameRef<PinballGame> {
|
|
||||||
/// {@macro flutter_forest}
|
|
||||||
FlutterForest() {
|
|
||||||
controller = _FlutterForestController(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
gameRef.addContactCallback(_DashNestBumperBallContactCallback());
|
|
||||||
|
|
||||||
final signpost = Signpost()..initialPosition = Vector2(8.35, -58.3);
|
|
||||||
|
|
||||||
final bigNest = _DashNestBumper.main()
|
|
||||||
..initialPosition = Vector2(18.55, -59.35);
|
|
||||||
final smallLeftNest = _DashNestBumper.a()
|
|
||||||
..initialPosition = Vector2(8.95, -51.95);
|
|
||||||
final smallRightNest = _DashNestBumper.b()
|
|
||||||
..initialPosition = Vector2(23.3, -46.75);
|
|
||||||
final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66);
|
|
||||||
|
|
||||||
await addAll([
|
|
||||||
signpost,
|
|
||||||
smallLeftNest,
|
|
||||||
smallRightNest,
|
|
||||||
bigNest,
|
|
||||||
dashAnimatronic,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FlutterForestController extends ComponentController<FlutterForest>
|
|
||||||
with HasGameRef<PinballGame> {
|
|
||||||
_FlutterForestController(FlutterForest flutterForest) : super(flutterForest);
|
|
||||||
|
|
||||||
final _activatedBumpers = <DashNestBumper>{};
|
|
||||||
|
|
||||||
void activateBumper(DashNestBumper dashNestBumper) {
|
|
||||||
if (!_activatedBumpers.add(dashNestBumper)) return;
|
|
||||||
|
|
||||||
dashNestBumper.activate();
|
|
||||||
|
|
||||||
final activatedBonus = _activatedBumpers.length == 3;
|
|
||||||
if (activatedBonus) {
|
|
||||||
_addBonusBall();
|
|
||||||
|
|
||||||
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.dashNest));
|
|
||||||
_activatedBumpers
|
|
||||||
..forEach((bumper) => bumper.deactivate())
|
|
||||||
..clear();
|
|
||||||
|
|
||||||
component.firstChild<DashAnimatronic>()?.playing = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _addBonusBall() async {
|
|
||||||
await gameRef.add(
|
|
||||||
ControlledBall.bonus(theme: gameRef.theme)
|
|
||||||
..initialPosition = Vector2(17.2, -52.7),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
|
|
||||||
// ContactCallback process is enhanced.
|
|
||||||
class _DashNestBumper extends DashNestBumper with ScorePoints {
|
|
||||||
_DashNestBumper.main() : super.main();
|
|
||||||
|
|
||||||
_DashNestBumper.a() : super.a();
|
|
||||||
|
|
||||||
_DashNestBumper.b() : super.b();
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get points => 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DashNestBumperBallContactCallback
|
|
||||||
extends ContactCallback<DashNestBumper, Ball> {
|
|
||||||
@override
|
|
||||||
void begin(DashNestBumper dashNestBumper, _, __) {
|
|
||||||
final parent = dashNestBumper.parent;
|
|
||||||
if (parent is FlutterForest) {
|
|
||||||
parent.controller.activateBumper(dashNestBumper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1 @@
|
|||||||
|
export 'flutter_forest_bonus_behavior.dart';
|
@ -0,0 +1,41 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
|
||||||
|
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
|
||||||
|
class FlutterForestBonusBehavior extends Component
|
||||||
|
with ParentIsA<FlutterForest>, HasGameRef<PinballGame> {
|
||||||
|
@override
|
||||||
|
void onMount() {
|
||||||
|
super.onMount();
|
||||||
|
|
||||||
|
final bumpers = parent.children.whereType<DashNestBumper>();
|
||||||
|
for (final bumper in bumpers) {
|
||||||
|
// TODO(alestiago): Refactor subscription management once the following is
|
||||||
|
// merged:
|
||||||
|
// https://github.com/flame-engine/flame/pull/1538
|
||||||
|
bumper.bloc.stream.listen((state) {
|
||||||
|
final achievedBonus = bumpers.every(
|
||||||
|
(bumper) => bumper.bloc.state == DashNestBumperState.active,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (achievedBonus) {
|
||||||
|
gameRef
|
||||||
|
.read<GameBloc>()
|
||||||
|
.add(const BonusActivated(GameBonus.dashNest));
|
||||||
|
gameRef.add(
|
||||||
|
ControlledBall.bonus(characterTheme: gameRef.characterTheme)
|
||||||
|
..initialPosition = Vector2(17.2, -52.7),
|
||||||
|
);
|
||||||
|
parent.firstChild<DashAnimatronic>()?.playing = true;
|
||||||
|
|
||||||
|
for (final bumper in bumpers) {
|
||||||
|
bumper.bloc.onReset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
// ignore_for_file: avoid_renaming_method_parameters
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// {@template flutter_forest}
|
||||||
|
/// Area positioned at the top right of the board where the [Ball] can bounce
|
||||||
|
/// off [DashNestBumper]s.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FlutterForest extends Component with ZIndex {
|
||||||
|
/// {@macro flutter_forest}
|
||||||
|
FlutterForest()
|
||||||
|
: super(
|
||||||
|
children: [
|
||||||
|
Signpost(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 20),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(8.35, -58.3),
|
||||||
|
DashNestBumper.main(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 200000),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(18.55, -59.35),
|
||||||
|
DashNestBumper.a(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 20000),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(8.95, -51.95),
|
||||||
|
DashNestBumper.b(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 20000),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(23.3, -46.75),
|
||||||
|
DashAnimatronic()..position = Vector2(20, -66),
|
||||||
|
FlutterForestBonusBehavior(),
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
zIndex = ZIndexes.flutterForest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [FlutterForest] without any children.
|
||||||
|
///
|
||||||
|
/// This can be used for testing [FlutterForest]'s behaviors in isolation.
|
||||||
|
@visibleForTesting
|
||||||
|
FlutterForest.test();
|
||||||
|
}
|
@ -1,101 +0,0 @@
|
|||||||
// ignore_for_file: avoid_renaming_method_parameters
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// {@template google_word}
|
|
||||||
/// Loads all [GoogleLetter]s to compose a [GoogleWord].
|
|
||||||
/// {@endtemplate}
|
|
||||||
class GoogleWord extends Component
|
|
||||||
with HasGameRef<PinballGame>, Controls<_GoogleWordController> {
|
|
||||||
/// {@macro google_word}
|
|
||||||
GoogleWord({
|
|
||||||
required Vector2 position,
|
|
||||||
}) : _position = position {
|
|
||||||
controller = _GoogleWordController(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Vector2 _position;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
gameRef.addContactCallback(_GoogleLetterBallContactCallback());
|
|
||||||
|
|
||||||
final offsets = [
|
|
||||||
Vector2(-12.92, 1.82),
|
|
||||||
Vector2(-8.33, -0.65),
|
|
||||||
Vector2(-2.88, -1.75),
|
|
||||||
Vector2(2.88, -1.75),
|
|
||||||
Vector2(8.33, -0.65),
|
|
||||||
Vector2(12.92, 1.82),
|
|
||||||
];
|
|
||||||
|
|
||||||
final letters = <GoogleLetter>[];
|
|
||||||
for (var index = 0; index < offsets.length; index++) {
|
|
||||||
letters.add(
|
|
||||||
GoogleLetter(index)..initialPosition = _position + offsets[index],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await addAll(letters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GoogleWordController extends ComponentController<GoogleWord>
|
|
||||||
with HasGameRef<PinballGame> {
|
|
||||||
_GoogleWordController(GoogleWord googleWord) : super(googleWord);
|
|
||||||
|
|
||||||
final _activatedLetters = <GoogleLetter>{};
|
|
||||||
|
|
||||||
Future<void> activate(GoogleLetter googleLetter) async {
|
|
||||||
if (!_activatedLetters.add(googleLetter)) return;
|
|
||||||
|
|
||||||
googleLetter.activate();
|
|
||||||
|
|
||||||
final activatedBonus = _activatedLetters.length == 6;
|
|
||||||
if (activatedBonus) {
|
|
||||||
gameRef.audio.googleBonus();
|
|
||||||
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.googleWord));
|
|
||||||
await _bonusAnimation();
|
|
||||||
_activatedLetters.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _bonusAnimation() async {
|
|
||||||
const blinkDuration = Duration(milliseconds: 300);
|
|
||||||
const blinkCount = 4;
|
|
||||||
final googleLetters = component.children.whereType<GoogleLetter>();
|
|
||||||
var shouldActivate = false;
|
|
||||||
|
|
||||||
await Future<void>.delayed(blinkDuration);
|
|
||||||
for (var i = 1; i < blinkCount * 2; i++) {
|
|
||||||
for (final letter in googleLetters) {
|
|
||||||
if (shouldActivate) {
|
|
||||||
letter.activate();
|
|
||||||
} else {
|
|
||||||
letter.deactivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
shouldActivate = !shouldActivate;
|
|
||||||
await Future<void>.delayed(blinkDuration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Activates a [GoogleLetter] when it contacts with a [Ball].
|
|
||||||
class _GoogleLetterBallContactCallback
|
|
||||||
extends ContactCallback<GoogleLetter, Ball> {
|
|
||||||
@override
|
|
||||||
void begin(GoogleLetter googleLetter, _, __) {
|
|
||||||
final parent = googleLetter.parent;
|
|
||||||
if (parent is GoogleWord) {
|
|
||||||
parent.controller.activate(googleLetter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1 @@
|
|||||||
|
export 'google_word_bonus_behavior.dart';
|
@ -0,0 +1,34 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated.
|
||||||
|
class GoogleWordBonusBehavior extends Component
|
||||||
|
with HasGameRef<PinballGame>, ParentIsA<GoogleWord> {
|
||||||
|
@override
|
||||||
|
void onMount() {
|
||||||
|
super.onMount();
|
||||||
|
|
||||||
|
final googleLetters = parent.children.whereType<GoogleLetter>();
|
||||||
|
for (final letter in googleLetters) {
|
||||||
|
// TODO(alestiago): Refactor subscription management once the following is
|
||||||
|
// merged:
|
||||||
|
// https://github.com/flame-engine/flame/pull/1538
|
||||||
|
letter.bloc.stream.listen((_) {
|
||||||
|
final achievedBonus = googleLetters
|
||||||
|
.every((letter) => letter.bloc.state == GoogleLetterState.active);
|
||||||
|
|
||||||
|
if (achievedBonus) {
|
||||||
|
gameRef.audio.googleBonus();
|
||||||
|
gameRef
|
||||||
|
.read<GameBloc>()
|
||||||
|
.add(const BonusActivated(GameBonus.googleWord));
|
||||||
|
for (final letter in googleLetters) {
|
||||||
|
letter.bloc.onReset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/game/components/google_word/behaviors/behaviors.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// {@template google_word}
|
||||||
|
/// Loads all [GoogleLetter]s to compose a [GoogleWord].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class GoogleWord extends Component with ZIndex {
|
||||||
|
/// {@macro google_word}
|
||||||
|
GoogleWord({
|
||||||
|
required Vector2 position,
|
||||||
|
}) : super(
|
||||||
|
children: [
|
||||||
|
GoogleLetter(
|
||||||
|
0,
|
||||||
|
children: [ScoringBehavior(points: 5000)],
|
||||||
|
)..initialPosition = position + Vector2(-12.92, 1.82),
|
||||||
|
GoogleLetter(
|
||||||
|
1,
|
||||||
|
children: [ScoringBehavior(points: 5000)],
|
||||||
|
)..initialPosition = position + Vector2(-8.33, -0.65),
|
||||||
|
GoogleLetter(
|
||||||
|
2,
|
||||||
|
children: [ScoringBehavior(points: 5000)],
|
||||||
|
)..initialPosition = position + Vector2(-2.88, -1.75),
|
||||||
|
GoogleLetter(
|
||||||
|
3,
|
||||||
|
children: [ScoringBehavior(points: 5000)],
|
||||||
|
)..initialPosition = position + Vector2(2.88, -1.75),
|
||||||
|
GoogleLetter(
|
||||||
|
4,
|
||||||
|
children: [ScoringBehavior(points: 5000)],
|
||||||
|
)..initialPosition = position + Vector2(8.33, -0.65),
|
||||||
|
GoogleLetter(
|
||||||
|
5,
|
||||||
|
children: [ScoringBehavior(points: 5000)],
|
||||||
|
)..initialPosition = position + Vector2(12.92, 1.82),
|
||||||
|
GoogleWordBonusBehavior(),
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
zIndex = ZIndexes.decal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [GoogleWord] without any children.
|
||||||
|
///
|
||||||
|
/// This can be used for testing [GoogleWord]'s behaviors in isolation.
|
||||||
|
@visibleForTesting
|
||||||
|
GoogleWord.test();
|
||||||
|
}
|
@ -1,21 +1,20 @@
|
|||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:pinball/game/components/components.dart';
|
import 'package:pinball/game/components/components.dart';
|
||||||
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// {@template launcher}
|
/// {@template launcher}
|
||||||
/// A [Blueprint] which creates the [Plunger], [RocketSpriteComponent] and
|
/// Channel on the right side of the board containing the [LaunchRamp],
|
||||||
/// [LaunchRamp].
|
/// [Plunger], and [RocketSpriteComponent].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class Launcher extends Blueprint {
|
class Launcher extends Component {
|
||||||
/// {@macro launcher}
|
/// {@macro launcher}
|
||||||
Launcher()
|
Launcher()
|
||||||
: super(
|
: super(
|
||||||
components: [
|
children: [
|
||||||
ControlledPlunger(compressionDistance: 14)
|
LaunchRamp(),
|
||||||
..initialPosition = Vector2(40.7, 38),
|
ControlledPlunger(compressionDistance: 10.5)
|
||||||
RocketSpriteComponent()..position = Vector2(43, 62),
|
..initialPosition = Vector2(41.1, 43),
|
||||||
|
RocketSpriteComponent()..position = Vector2(43, 62.3),
|
||||||
],
|
],
|
||||||
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,47 @@
|
|||||||
|
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';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// {@template multipliers}
|
||||||
|
/// A group for the multipliers on the board.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Multipliers extends Component with ZIndex {
|
||||||
|
/// {@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(),
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
zIndex = ZIndexes.decal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates [Multipliers] without any children.
|
||||||
|
///
|
||||||
|
/// This can be used for testing [Multipliers]'s behaviors in isolation.
|
||||||
|
@visibleForTesting
|
||||||
|
Multipliers.test();
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
// ignore_for_file: avoid_renaming_method_parameters
|
|
||||||
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
|
|
||||||
/// {@template score_points}
|
|
||||||
/// Specifies the amount of points received on [Ball] collision.
|
|
||||||
/// {@endtemplate}
|
|
||||||
mixin ScorePoints<T extends Forge2DGame> on BodyComponent<T> {
|
|
||||||
/// {@macro score_points}
|
|
||||||
int get points;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
body.userData = this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template ball_score_points_callbacks}
|
|
||||||
/// Adds points to the score when a [Ball] collides with a [BodyComponent] that
|
|
||||||
/// implements [ScorePoints].
|
|
||||||
/// {@endtemplate}
|
|
||||||
class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
|
|
||||||
/// {@macro ball_score_points_callbacks}
|
|
||||||
BallScorePointsCallback(PinballGame game) : _gameRef = game;
|
|
||||||
|
|
||||||
final PinballGame _gameRef;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void begin(
|
|
||||||
Ball ball,
|
|
||||||
ScorePoints scorePoints,
|
|
||||||
Contact _,
|
|
||||||
) {
|
|
||||||
_gameRef.read<GameBloc>().add(Scored(points: scorePoints.points));
|
|
||||||
_gameRef.audio.score();
|
|
||||||
|
|
||||||
_gameRef.add(
|
|
||||||
ScoreText(
|
|
||||||
text: scorePoints.points.toString(),
|
|
||||||
position: ball.body.position,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,34 @@
|
|||||||
|
// ignore_for_file: avoid_renaming_method_parameters
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// {@template scoring_behavior}
|
||||||
|
/// Adds points to the score when the ball contacts the [parent].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> {
|
||||||
|
/// {@macro scoring_behavior}
|
||||||
|
ScoringBehavior({
|
||||||
|
required int points,
|
||||||
|
}) : _points = points;
|
||||||
|
|
||||||
|
final int _points;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void beginContact(Object other, Contact contact) {
|
||||||
|
super.beginContact(other, contact);
|
||||||
|
if (other is! Ball) return;
|
||||||
|
|
||||||
|
gameRef.read<GameBloc>().add(Scored(points: _points));
|
||||||
|
gameRef.audio.score();
|
||||||
|
gameRef.firstChild<ZCanvasComponent>()!.add(
|
||||||
|
ScoreText(
|
||||||
|
text: _points.toString(),
|
||||||
|
position: other.body.position,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,110 +0,0 @@
|
|||||||
// ignore_for_file: avoid_renaming_method_parameters
|
|
||||||
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// {@template sparky_fire_zone}
|
|
||||||
/// Area positioned at the top left of the [Board] where the [Ball]
|
|
||||||
/// can bounce off [SparkyBumper]s.
|
|
||||||
///
|
|
||||||
/// When a [Ball] hits [SparkyBumper]s, the bumper animates.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class SparkyFireZone extends Blueprint {
|
|
||||||
/// {@macro sparky_fire_zone}
|
|
||||||
SparkyFireZone()
|
|
||||||
: super(
|
|
||||||
components: [
|
|
||||||
_SparkyBumper.a()..initialPosition = Vector2(-22.9, -41.65),
|
|
||||||
_SparkyBumper.b()..initialPosition = Vector2(-21.25, -57.9),
|
|
||||||
_SparkyBumper.c()..initialPosition = Vector2(-3.3, -52.55),
|
|
||||||
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8),
|
|
||||||
SparkyAnimatronic()..position = Vector2(-13.8, -58.2),
|
|
||||||
],
|
|
||||||
blueprints: [
|
|
||||||
SparkyComputer(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
|
|
||||||
// ContactCallback process is enhanced.
|
|
||||||
class _SparkyBumper extends SparkyBumper with ScorePoints {
|
|
||||||
_SparkyBumper.a() : super.a();
|
|
||||||
|
|
||||||
_SparkyBumper.b() : super.b();
|
|
||||||
|
|
||||||
_SparkyBumper.c() : super.c();
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get points => 20;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
// TODO(alestiago): Revisit once this has been merged:
|
|
||||||
// https://github.com/flame-engine/flame/pull/1547
|
|
||||||
gameRef.addContactCallback(SparkyBumperBallContactCallback());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Listens when a [Ball] bounces bounces against a [SparkyBumper].
|
|
||||||
@visibleForTesting
|
|
||||||
class SparkyBumperBallContactCallback
|
|
||||||
extends ContactCallback<SparkyBumper, Ball> {
|
|
||||||
@override
|
|
||||||
void begin(
|
|
||||||
SparkyBumper sparkyBumper,
|
|
||||||
Ball _,
|
|
||||||
Contact __,
|
|
||||||
) {
|
|
||||||
sparkyBumper.animate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template sparky_computer_sensor}
|
|
||||||
/// Small sensor body used to detect when a ball has entered the
|
|
||||||
/// [SparkyComputer].
|
|
||||||
/// {@endtemplate}
|
|
||||||
// TODO(alestiago): Revisit once this has been merged:
|
|
||||||
// https://github.com/flame-engine/flame/pull/1547
|
|
||||||
class SparkyComputerSensor extends BodyComponent with InitialPosition {
|
|
||||||
/// {@macro sparky_computer_sensor}
|
|
||||||
SparkyComputerSensor() {
|
|
||||||
renderBody = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Body createBody() {
|
|
||||||
final shape = CircleShape()..radius = 0.1;
|
|
||||||
final fixtureDef = FixtureDef(shape, isSensor: true);
|
|
||||||
final bodyDef = BodyDef(
|
|
||||||
position: initialPosition,
|
|
||||||
userData: this,
|
|
||||||
);
|
|
||||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
// TODO(alestiago): Revisit once this has been merged:
|
|
||||||
// https://github.com/flame-engine/flame/pull/1547
|
|
||||||
gameRef.addContactCallback(SparkyComputerSensorBallContactCallback());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
// TODO(alestiago): Revisit once this has been merged:
|
|
||||||
// https://github.com/flame-engine/flame/pull/1547
|
|
||||||
// ignore: public_member_api_docs
|
|
||||||
class SparkyComputerSensorBallContactCallback
|
|
||||||
extends ContactCallback<SparkyComputerSensor, ControlledBall> {
|
|
||||||
@override
|
|
||||||
void begin(_, ControlledBall controlledBall, __) {
|
|
||||||
controlledBall.controller.turboCharge();
|
|
||||||
controlledBall.gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,73 @@
|
|||||||
|
// ignore_for_file: avoid_renaming_method_parameters
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template sparky_scorch}
|
||||||
|
/// Area positioned at the top left of the board containing the
|
||||||
|
/// [SparkyComputer], [SparkyAnimatronic], and [SparkyBumper]s.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class SparkyScorch extends Component {
|
||||||
|
/// {@macro sparky_scorch}
|
||||||
|
SparkyScorch()
|
||||||
|
: super(
|
||||||
|
children: [
|
||||||
|
SparkyBumper.a(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 20000),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(-22.9, -41.65),
|
||||||
|
SparkyBumper.b(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 20000),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(-21.25, -57.9),
|
||||||
|
SparkyBumper.c(
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 20000),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(-3.3, -52.55),
|
||||||
|
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8),
|
||||||
|
SparkyAnimatronic()..position = Vector2(-13.8, -58.2),
|
||||||
|
SparkyComputer(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template sparky_computer_sensor}
|
||||||
|
/// Small sensor body used to detect when a ball has entered the
|
||||||
|
/// [SparkyComputer].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class SparkyComputerSensor extends BodyComponent
|
||||||
|
with InitialPosition, ContactCallbacks {
|
||||||
|
/// {@macro sparky_computer_sensor}
|
||||||
|
SparkyComputerSensor()
|
||||||
|
: super(
|
||||||
|
renderBody: false,
|
||||||
|
children: [
|
||||||
|
ScoringBehavior(points: 200000),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = CircleShape()..radius = 0.1;
|
||||||
|
final fixtureDef = FixtureDef(shape, isSensor: true);
|
||||||
|
final bodyDef = BodyDef(
|
||||||
|
position: initialPosition,
|
||||||
|
userData: this,
|
||||||
|
);
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void beginContact(Object other, Contact contact) {
|
||||||
|
super.beginContact(other, contact);
|
||||||
|
if (other is! ControlledBall) return;
|
||||||
|
|
||||||
|
other.controller.turboCharge();
|
||||||
|
gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
|
||||||
|
}
|
||||||
|
}
|
@ -1,66 +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.
|
|
||||||
///
|
|
||||||
/// Collisions with [BottomWall] are listened by
|
|
||||||
/// [BottomWallBallContactCallback].
|
|
||||||
/// {@endtemplate}
|
|
||||||
class BottomWall extends Wall {
|
|
||||||
/// {@macro bottom_wall}
|
|
||||||
BottomWall()
|
|
||||||
: super(
|
|
||||||
start: BoardDimensions.bounds.bottomLeft.toVector2(),
|
|
||||||
end: BoardDimensions.bounds.bottomRight.toVector2(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template bottom_wall_ball_contact_callback}
|
|
||||||
/// Listens when a [ControlledBall] falls into a [BottomWall].
|
|
||||||
/// {@endtemplate}
|
|
||||||
class BottomWallBallContactCallback
|
|
||||||
extends ContactCallback<ControlledBall, BottomWall> {
|
|
||||||
@override
|
|
||||||
void begin(ControlledBall ball, BottomWall wall, Contact contact) {
|
|
||||||
ball.controller.lost();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +1,122 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:pinball/game/game.dart';
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball/gen/gen.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
/// {@template game_hud}
|
/// {@template game_hud}
|
||||||
/// Overlay of a [PinballGame] that displays the current [GameState.score] and
|
/// Overlay on the [PinballGame].
|
||||||
/// [GameState.balls].
|
///
|
||||||
|
/// Displays the current [GameState.score], [GameState.rounds] and animates when
|
||||||
|
/// the player gets a [GameBonus].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class GameHud extends StatelessWidget {
|
class GameHud extends StatefulWidget {
|
||||||
/// {@macro game_hud}
|
/// {@macro game_hud}
|
||||||
const GameHud({Key? key}) : super(key: key);
|
const GameHud({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GameHud> createState() => _GameHudState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GameHudState extends State<GameHud> {
|
||||||
|
bool showAnimation = false;
|
||||||
|
|
||||||
|
/// Ratio from sprite frame (width 500, height 144) w / h = ratio
|
||||||
|
static const _ratio = 3.47;
|
||||||
|
static const _width = 265.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final state = context.watch<GameBloc>().state;
|
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
|
||||||
|
|
||||||
return Container(
|
return _ScoreViewDecoration(
|
||||||
color: Colors.redAccent,
|
child: SizedBox(
|
||||||
width: 200,
|
height: _width / _ratio,
|
||||||
height: 100,
|
width: _width,
|
||||||
padding: const EdgeInsets.all(16),
|
child: BlocListener<GameBloc, GameState>(
|
||||||
child: Row(
|
listenWhen: (previous, current) =>
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
previous.bonusHistory.length != current.bonusHistory.length,
|
||||||
children: [
|
listener: (_, __) => setState(() => showAnimation = true),
|
||||||
Text(
|
child: AnimatedSwitcher(
|
||||||
'${state.score}',
|
duration: kThemeAnimationDuration,
|
||||||
style: Theme.of(context).textTheme.headline3,
|
child: showAnimation && !isGameOver
|
||||||
|
? _AnimationView(
|
||||||
|
onComplete: () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => showAnimation = false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const ScoreView(),
|
||||||
),
|
),
|
||||||
Wrap(
|
),
|
||||||
direction: Axis.vertical,
|
),
|
||||||
children: [
|
);
|
||||||
for (var i = 0; i < state.balls; i++)
|
}
|
||||||
const Padding(
|
}
|
||||||
padding: EdgeInsets.only(top: 6, right: 6),
|
|
||||||
child: CircleAvatar(
|
class _ScoreViewDecoration extends StatelessWidget {
|
||||||
radius: 8,
|
const _ScoreViewDecoration({
|
||||||
backgroundColor: Colors.black,
|
Key? key,
|
||||||
),
|
required this.child,
|
||||||
),
|
}) : super(key: key);
|
||||||
],
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const radius = BorderRadius.all(Radius.circular(12));
|
||||||
|
const borderWidth = 5.0;
|
||||||
|
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: radius,
|
||||||
|
border: Border.all(
|
||||||
|
color: PinballColors.white,
|
||||||
|
width: borderWidth,
|
||||||
|
),
|
||||||
|
image: DecorationImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: AssetImage(
|
||||||
|
Assets.images.score.miniScoreBackground.path,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(borderWidth - 1),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: radius,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimationView extends StatelessWidget {
|
||||||
|
const _AnimationView({
|
||||||
|
Key? key,
|
||||||
|
required this.onComplete,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final VoidCallback onComplete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final lastBonus = context.select(
|
||||||
|
(GameBloc bloc) => bloc.state.bonusHistory.last,
|
||||||
);
|
);
|
||||||
|
switch (lastBonus) {
|
||||||
|
case GameBonus.dashNest:
|
||||||
|
return BonusAnimation.dashNest(onCompleted: onComplete);
|
||||||
|
case GameBonus.sparkyTurboCharge:
|
||||||
|
return BonusAnimation.sparkyTurboCharge(onCompleted: onComplete);
|
||||||
|
case GameBonus.dinoChomp:
|
||||||
|
return BonusAnimation.dinoChomp(onCompleted: onComplete);
|
||||||
|
case GameBonus.googleWord:
|
||||||
|
return BonusAnimation.googleWord(onCompleted: onComplete);
|
||||||
|
case GameBonus.androidSpaceship:
|
||||||
|
return BonusAnimation.androidSpaceship(onCompleted: onComplete);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
/// {@template round_count_display}
|
||||||
|
/// Colored square indicating if a round is available.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class RoundCountDisplay extends StatelessWidget {
|
||||||
|
/// {@macro round_count_display}
|
||||||
|
const RoundCountDisplay({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final rounds = context.select((GameBloc bloc) => bloc.state.rounds);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.rounds,
|
||||||
|
style: Theme.of(context).textTheme.subtitle1,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
RoundIndicator(isActive: rounds >= 1),
|
||||||
|
RoundIndicator(isActive: rounds >= 2),
|
||||||
|
RoundIndicator(isActive: rounds >= 3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template round_indicator}
|
||||||
|
/// [Widget] that displays the round indicator.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@visibleForTesting
|
||||||
|
class RoundIndicator extends StatelessWidget {
|
||||||
|
/// {@macro round_indicator}
|
||||||
|
const RoundIndicator({
|
||||||
|
Key? key,
|
||||||
|
required this.isActive,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// A value that describes whether the indicator is active.
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color =
|
||||||
|
isActive ? PinballColors.yellow : PinballColors.yellow.withAlpha(128);
|
||||||
|
const size = 8.0;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Container(
|
||||||
|
color: color,
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template score_view}
|
||||||
|
/// [Widget] that displays the score.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class ScoreView extends StatelessWidget {
|
||||||
|
/// {@macro score_view}
|
||||||
|
const ScoreView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: kThemeAnimationDuration,
|
||||||
|
child: isGameOver ? const _GameOver() : const _ScoreDisplay(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GameOver extends StatelessWidget {
|
||||||
|
const _GameOver({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
l10n.gameOver,
|
||||||
|
style: Theme.of(context).textTheme.headline1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScoreDisplay extends StatelessWidget {
|
||||||
|
const _ScoreDisplay({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.score.toLowerCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle1,
|
||||||
|
),
|
||||||
|
const _ScoreText(),
|
||||||
|
const RoundCountDisplay(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScoreText extends StatelessWidget {
|
||||||
|
const _ScoreText({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final score = context.select((GameBloc bloc) => bloc.state.score);
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
score.formatScore(),
|
||||||
|
style: Theme.of(context).textTheme.headline1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
export 'bonus_animation.dart';
|
export 'bonus_animation.dart';
|
||||||
export 'game_hud.dart';
|
export 'game_hud.dart';
|
||||||
export 'play_button_overlay.dart';
|
export 'play_button_overlay.dart';
|
||||||
|
export 'round_count_display.dart';
|
||||||
|
export 'score_view.dart';
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export 'widgets/widgets.dart';
|
@ -0,0 +1,305 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/gen/gen.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
import 'package:platform_helper/platform_helper.dart';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showHowToPlayDialog(BuildContext context) {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => HowToPlayDialog(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class HowToPlayDialog extends StatefulWidget {
|
||||||
|
HowToPlayDialog({
|
||||||
|
Key? key,
|
||||||
|
@visibleForTesting PlatformHelper? platformHelper,
|
||||||
|
}) : platformHelper = platformHelper ?? PlatformHelper(),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
final PlatformHelper platformHelper;
|
||||||
|
|
||||||
|
@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).pop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
closeTimer.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isMobile = widget.platformHelper.isMobile;
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return PinballDialog(
|
||||||
|
title: l10n.howToPlay,
|
||||||
|
subtitle: l10n.tipsForFlips,
|
||||||
|
child: isMobile ? const _MobileBody() : const _DesktopBody(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: paddingWidth,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
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;
|
||||||
|
final headline3 = Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.headline3!
|
||||||
|
.copyWith(color: PinballColors.white);
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(l10n.tapAndHoldRocket, style: headline3),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: '${l10n.to} ', style: headline3),
|
||||||
|
TextSpan(
|
||||||
|
text: l10n.launch,
|
||||||
|
style: headline3.copyWith(color: PinballColors.blue),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileFlipperControls extends StatelessWidget {
|
||||||
|
const _MobileFlipperControls({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final headline3 = Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.headline3!
|
||||||
|
.copyWith(color: PinballColors.white);
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(l10n.tapLeftRightScreen, style: headline3),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: '${l10n.to} ', style: headline3),
|
||||||
|
TextSpan(
|
||||||
|
text: l10n.flip,
|
||||||
|
style: headline3.copyWith(color: PinballColors.orange),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopBody extends StatelessWidget {
|
||||||
|
const _DesktopBody({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
children: const [
|
||||||
|
SizedBox(height: 16),
|
||||||
|
_DesktopLaunchControls(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
_DesktopFlipperControls(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopLaunchControls extends StatelessWidget {
|
||||||
|
const _DesktopLaunchControls({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.launchControls,
|
||||||
|
style: Theme.of(context).textTheme.headline4,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Wrap(
|
||||||
|
children: const [
|
||||||
|
_KeyButton(control: Control.down),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
_KeyButton(control: Control.space),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
_KeyButton(control: Control.s),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopFlipperControls extends StatelessWidget {
|
||||||
|
const _DesktopFlipperControls({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.flipperControls,
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
_KeyButton(control: Control.left),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
_KeyButton(control: Control.right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
children: const [
|
||||||
|
_KeyButton(control: Control.a),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
_KeyButton(control: Control.d),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KeyButton extends StatelessWidget {
|
||||||
|
const _KeyButton({Key? key, required this.control}) : super(key: key);
|
||||||
|
|
||||||
|
final Control control;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
final textStyle =
|
||||||
|
control.isArrow ? textTheme.headline1 : textTheme.headline3;
|
||||||
|
const height = 60.0;
|
||||||
|
final width = control.isSpace ? height * 2.83 : height;
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
image: AssetImage(
|
||||||
|
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: PinballColors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"@@locale": "es",
|
|
||||||
"play": "Jugar",
|
|
||||||
"@play": {
|
|
||||||
"description": "Text displayed on the landing page play button"
|
|
||||||
},
|
|
||||||
"start": "Comienzo",
|
|
||||||
"@start": {
|
|
||||||
"description": "Text displayed on the character selection page start button"
|
|
||||||
},
|
|
||||||
"characterSelectionTitle": "¡Elige a tu personaje!",
|
|
||||||
"@characterSelectionTitle": {
|
|
||||||
"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/theme/theme.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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,23 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
// TODO(allisonryan0002): Document this section when the API is stable.
|
||||||
|
|
||||||
|
part of 'character_theme_cubit.dart';
|
||||||
|
|
||||||
|
class CharacterThemeState extends Equatable {
|
||||||
|
const CharacterThemeState(this.characterTheme);
|
||||||
|
|
||||||
|
const CharacterThemeState.initial() : characterTheme = const DashTheme();
|
||||||
|
|
||||||
|
final CharacterTheme characterTheme;
|
||||||
|
|
||||||
|
bool get isSparkySelected => characterTheme == const SparkyTheme();
|
||||||
|
|
||||||
|
bool get isDashSelected => characterTheme == const DashTheme();
|
||||||
|
|
||||||
|
bool get isAndroidSelected => characterTheme == const AndroidTheme();
|
||||||
|
|
||||||
|
bool get isDinoSelected => characterTheme == const DinoTheme();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [characterTheme];
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
export 'cubit/character_theme_cubit.dart';
|
||||||
|
export 'view/view.dart';
|
@ -0,0 +1,162 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:pinball/how_to_play/how_to_play.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball/select_character/cubit/character_theme_cubit.dart';
|
||||||
|
import 'package:pinball/select_character/select_character.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
/// Inflates [CharacterSelectionDialog] using [showDialog].
|
||||||
|
Future<void> showCharacterSelectionDialog(BuildContext context) {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => const CharacterSelectionDialog(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template character_selection_dialog}
|
||||||
|
/// Dialog used to select the playing character of the game.
|
||||||
|
/// {@endtemplate character_selection_dialog}
|
||||||
|
class CharacterSelectionDialog extends StatelessWidget {
|
||||||
|
/// {@macro character_selection_dialog}
|
||||||
|
const CharacterSelectionDialog({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return PinballDialog(
|
||||||
|
title: l10n.characterSelectionTitle,
|
||||||
|
subtitle: l10n.characterSelectionSubtitle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _CharacterPreview()),
|
||||||
|
Expanded(child: _CharacterGrid()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const _SelectCharacterButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectCharacterButton extends StatelessWidget {
|
||||||
|
const _SelectCharacterButton({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return PinballButton(
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
await showHowToPlayDialog(context);
|
||||||
|
},
|
||||||
|
text: l10n.select,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CharacterGrid extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<CharacterThemeCubit, CharacterThemeState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
_Character(
|
||||||
|
key: const Key('sparky_character_selection'),
|
||||||
|
character: const SparkyTheme(),
|
||||||
|
isSelected: state.isSparkySelected,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_Character(
|
||||||
|
key: const Key('android_character_selection'),
|
||||||
|
character: const AndroidTheme(),
|
||||||
|
isSelected: state.isAndroidSelected,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
_Character(
|
||||||
|
key: const Key('dash_character_selection'),
|
||||||
|
character: const DashTheme(),
|
||||||
|
isSelected: state.isDashSelected,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_Character(
|
||||||
|
key: const Key('dino_character_selection'),
|
||||||
|
character: const DinoTheme(),
|
||||||
|
isSelected: state.isDinoSelected,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CharacterPreview extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<CharacterThemeCubit, CharacterThemeState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
state.characterTheme.name,
|
||||||
|
style: Theme.of(context).textTheme.headline2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Expanded(child: state.characterTheme.icon.image()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Character extends StatelessWidget {
|
||||||
|
const _Character({
|
||||||
|
Key? key,
|
||||||
|
required this.character,
|
||||||
|
required this.isSelected,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme character;
|
||||||
|
final bool isSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: isSelected ? 1 : 0.3,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () =>
|
||||||
|
context.read<CharacterThemeCubit>().characterSelected(character),
|
||||||
|
child: character.icon.image(fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,161 +0,0 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:pinball/l10n/l10n.dart';
|
|
||||||
|
|
||||||
class HowToPlayDialog extends StatelessWidget {
|
|
||||||
const HowToPlayDialog({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = context.l10n;
|
|
||||||
const spacing = SizedBox(height: 16);
|
|
||||||
|
|
||||||
return Dialog(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(l10n.howToPlay),
|
|
||||||
spacing,
|
|
||||||
const _LaunchControls(),
|
|
||||||
spacing,
|
|
||||||
const _FlipperControls(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LaunchControls extends StatelessWidget {
|
|
||||||
const _LaunchControls({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = context.l10n;
|
|
||||||
const spacing = SizedBox(width: 10);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Text(l10n.launchControls),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: const [
|
|
||||||
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down),
|
|
||||||
spacing,
|
|
||||||
KeyIndicator.fromKeyName(keyName: 'SPACE'),
|
|
||||||
spacing,
|
|
||||||
KeyIndicator.fromKeyName(keyName: 'S'),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FlipperControls extends StatelessWidget {
|
|
||||||
const _FlipperControls({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = context.l10n;
|
|
||||||
const rowSpacing = SizedBox(width: 20);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Text(l10n.flipperControls),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: const [
|
|
||||||
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left),
|
|
||||||
rowSpacing,
|
|
||||||
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: const [
|
|
||||||
KeyIndicator.fromKeyName(keyName: 'A'),
|
|
||||||
rowSpacing,
|
|
||||||
KeyIndicator.fromKeyName(keyName: 'D'),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(allisonryan0002): remove visibility when adding final UI.
|
|
||||||
@visibleForTesting
|
|
||||||
class KeyIndicator extends StatelessWidget {
|
|
||||||
const KeyIndicator._({
|
|
||||||
Key? key,
|
|
||||||
required String keyName,
|
|
||||||
required IconData keyIcon,
|
|
||||||
required bool fromIcon,
|
|
||||||
}) : _keyName = keyName,
|
|
||||||
_keyIcon = keyIcon,
|
|
||||||
_fromIcon = fromIcon,
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
const KeyIndicator.fromKeyName({Key? key, required String keyName})
|
|
||||||
: this._(
|
|
||||||
key: key,
|
|
||||||
keyName: keyName,
|
|
||||||
keyIcon: Icons.keyboard_arrow_down,
|
|
||||||
fromIcon: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const KeyIndicator.fromIcon({Key? key, required IconData keyIcon})
|
|
||||||
: this._(
|
|
||||||
key: key,
|
|
||||||
keyName: '',
|
|
||||||
keyIcon: keyIcon,
|
|
||||||
fromIcon: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
final String _keyName;
|
|
||||||
|
|
||||||
final IconData _keyIcon;
|
|
||||||
|
|
||||||
final bool _fromIcon;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const iconPadding = EdgeInsets.all(15);
|
|
||||||
const textPadding = EdgeInsets.symmetric(vertical: 20, horizontal: 22);
|
|
||||||
final boarderColor = Colors.blue.withOpacity(0.5);
|
|
||||||
final color = Colors.blue.withOpacity(0.7);
|
|
||||||
|
|
||||||
return DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
border: Border.all(
|
|
||||||
color: boarderColor,
|
|
||||||
width: 3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: _fromIcon
|
|
||||||
? Padding(
|
|
||||||
padding: iconPadding,
|
|
||||||
child: Icon(_keyIcon, color: color),
|
|
||||||
)
|
|
||||||
: Padding(
|
|
||||||
padding: textPadding,
|
|
||||||
child: Text(_keyName, style: TextStyle(color: color)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:pinball/theme/theme.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
|
|
||||||
const _fontPackage = 'pinball_components';
|
|
||||||
const _primaryFontFamily = PinballFonts.pixeloidSans;
|
|
||||||
|
|
||||||
abstract class AppTextStyle {
|
|
||||||
static const headline1 = TextStyle(
|
|
||||||
fontSize: 28,
|
|
||||||
package: _fontPackage,
|
|
||||||
fontFamily: _primaryFontFamily,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const headline2 = TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
package: _fontPackage,
|
|
||||||
fontFamily: _primaryFontFamily,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const headline3 = TextStyle(
|
|
||||||
color: AppColors.white,
|
|
||||||
fontSize: 20,
|
|
||||||
package: _fontPackage,
|
|
||||||
fontFamily: _primaryFontFamily,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const subtitle1 = TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontFamily: _primaryFontFamily,
|
|
||||||
package: _fontPackage,
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
|
||||||
// TODO(allisonryan0002): Document this section when the API is stable.
|
|
||||||
|
|
||||||
part of 'theme_cubit.dart';
|
|
||||||
|
|
||||||
class ThemeState extends Equatable {
|
|
||||||
const ThemeState(this.theme);
|
|
||||||
|
|
||||||
const ThemeState.initial()
|
|
||||||
: theme = const PinballTheme(characterTheme: DashTheme());
|
|
||||||
|
|
||||||
final PinballTheme theme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [theme];
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export 'app_colors.dart';
|
|
||||||
export 'app_text_style.dart';
|
|
||||||
export 'cubit/theme_cubit.dart';
|
|
||||||
export 'view/view.dart';
|
|
@ -1,132 +0,0 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:pinball/l10n/l10n.dart';
|
|
||||||
import 'package:pinball/start_game/start_game.dart';
|
|
||||||
import 'package:pinball/theme/theme.dart';
|
|
||||||
import 'package:pinball_theme/pinball_theme.dart';
|
|
||||||
|
|
||||||
class CharacterSelectionDialog extends StatelessWidget {
|
|
||||||
const CharacterSelectionDialog({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
static Route route() {
|
|
||||||
return MaterialPageRoute<void>(
|
|
||||||
builder: (_) => const CharacterSelectionDialog(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (_) => ThemeCubit(),
|
|
||||||
child: const CharacterSelectionView(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CharacterSelectionView extends StatelessWidget {
|
|
||||||
const CharacterSelectionView({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = context.l10n;
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 80),
|
|
||||||
Text(
|
|
||||||
l10n.characterSelectionTitle,
|
|
||||||
style: Theme.of(context).textTheme.headline3,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 80),
|
|
||||||
const _CharacterSelectionGridView(),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => const HowToPlayDialog(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(l10n.start),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CharacterSelectionGridView extends StatelessWidget {
|
|
||||||
const _CharacterSelectionGridView({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: GridView.count(
|
|
||||||
shrinkWrap: true,
|
|
||||||
crossAxisCount: 2,
|
|
||||||
mainAxisSpacing: 20,
|
|
||||||
crossAxisSpacing: 20,
|
|
||||||
children: const [
|
|
||||||
CharacterImageButton(
|
|
||||||
DashTheme(),
|
|
||||||
key: Key('characterSelectionPage_dashButton'),
|
|
||||||
),
|
|
||||||
CharacterImageButton(
|
|
||||||
SparkyTheme(),
|
|
||||||
key: Key('characterSelectionPage_sparkyButton'),
|
|
||||||
),
|
|
||||||
CharacterImageButton(
|
|
||||||
AndroidTheme(),
|
|
||||||
key: Key('characterSelectionPage_androidButton'),
|
|
||||||
),
|
|
||||||
CharacterImageButton(
|
|
||||||
DinoTheme(),
|
|
||||||
key: Key('characterSelectionPage_dinoButton'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(allisonryan0002): remove visibility when adding final UI.
|
|
||||||
@visibleForTesting
|
|
||||||
class CharacterImageButton extends StatelessWidget {
|
|
||||||
const CharacterImageButton(
|
|
||||||
this.characterTheme, {
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final CharacterTheme characterTheme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final currentCharacterTheme = context.select<ThemeCubit, CharacterTheme>(
|
|
||||||
(cubit) => cubit.state.theme.characterTheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => context.read<ThemeCubit>().characterSelected(characterTheme),
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: (currentCharacterTheme == characterTheme)
|
|
||||||
? Colors.blue.withOpacity(0.5)
|
|
||||||
: null,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: characterTheme.icon.image(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,39 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# VSCode related
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Web related
|
||||||
|
lib/generated_plugin_registrant.dart
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
@ -0,0 +1,11 @@
|
|||||||
|
# authentication_repository
|
||||||
|
|
||||||
|
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
|
||||||
|
[![License: MIT][license_badge]][license_link]
|
||||||
|
|
||||||
|
Repository to manage user authentication.
|
||||||
|
|
||||||
|
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||||
|
[license_link]: https://opensource.org/licenses/MIT
|
||||||
|
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
|
||||||
|
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
|
@ -0,0 +1 @@
|
|||||||
|
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
@ -0,0 +1,3 @@
|
|||||||
|
library authentication_repository;
|
||||||
|
|
||||||
|
export 'src/authentication_repository.dart';
|
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
|
||||||
|
/// {@template authentication_exception}
|
||||||
|
/// Exception for authentication repository failures.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class AuthenticationException implements Exception {
|
||||||
|
/// {@macro authentication_exception}
|
||||||
|
const AuthenticationException(this.error, this.stackTrace);
|
||||||
|
|
||||||
|
/// The error that was caught.
|
||||||
|
final Object error;
|
||||||
|
|
||||||
|
/// The Stacktrace associated with the [error].
|
||||||
|
final StackTrace stackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template authentication_repository}
|
||||||
|
/// Repository to manage user authentication.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class AuthenticationRepository {
|
||||||
|
/// {@macro authentication_repository}
|
||||||
|
AuthenticationRepository(this._firebaseAuth);
|
||||||
|
|
||||||
|
final FirebaseAuth _firebaseAuth;
|
||||||
|
|
||||||
|
/// Sign in the existing user anonymously using [FirebaseAuth]. If the
|
||||||
|
/// authentication process can't be completed, it will throw an
|
||||||
|
/// [AuthenticationException].
|
||||||
|
Future<void> authenticateAnonymously() async {
|
||||||
|
try {
|
||||||
|
await _firebaseAuth.signInAnonymously();
|
||||||
|
} on Exception catch (error, stackTrace) {
|
||||||
|
throw AuthenticationException(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
name: authentication_repository
|
||||||
|
description: Repository to manage user authentication.
|
||||||
|
version: 1.0.0+1
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.16.0 <3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
firebase_auth: ^3.3.16
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
mocktail: ^0.2.0
|
||||||
|
very_good_analysis: ^2.4.0
|
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:authentication_repository/authentication_repository.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class MockFirebaseAuth extends Mock implements FirebaseAuth {}
|
||||||
|
|
||||||
|
class MockUserCredential extends Mock implements UserCredential {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late FirebaseAuth firebaseAuth;
|
||||||
|
late UserCredential userCredential;
|
||||||
|
late AuthenticationRepository authenticationRepository;
|
||||||
|
|
||||||
|
group('AuthenticationRepository', () {
|
||||||
|
setUp(() {
|
||||||
|
firebaseAuth = MockFirebaseAuth();
|
||||||
|
userCredential = MockUserCredential();
|
||||||
|
authenticationRepository = AuthenticationRepository(firebaseAuth);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('authenticateAnonymously', () {
|
||||||
|
test('completes if no exception is thrown', () async {
|
||||||
|
when(() => firebaseAuth.signInAnonymously())
|
||||||
|
.thenAnswer((_) async => userCredential);
|
||||||
|
await authenticationRepository.authenticateAnonymously();
|
||||||
|
verify(() => firebaseAuth.signInAnonymously()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws AuthenticationException when firebase auth fails', () async {
|
||||||
|
when(() => firebaseAuth.signInAnonymously())
|
||||||
|
.thenThrow(Exception('oops'));
|
||||||
|
expect(
|
||||||
|
() => authenticationRepository.authenticateAnonymously(),
|
||||||
|
throwsA(isA<AuthenticationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -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,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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |