Merge branch 'main' into fix/game-hud-position

pull/289/head
arturplaczek 3 years ago committed by GitHub
commit 2e2fb2a5fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -10,13 +10,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Deploy Development name: Deploy Development
steps: steps:
- uses: actions/checkout@v2 - name: Checkout Repo
- uses: subosito/flutter-action@v2 uses: actions/checkout@v2
- name: Setup Flutter
uses: subosito/flutter-action@v2
with: with:
channel: stable channel: stable
- run: flutter packages get
- run: flutter build web --target lib/main_development.dart --web-renderer canvaskit --release - name: Build Flutter App
- uses: FirebaseExtended/action-hosting-deploy@v0 run: |
flutter packages get
flutter build web --target lib/main_development.dart --web-renderer canvaskit --release
- name: Deploy to Firebase
uses: FirebaseExtended/action-hosting-deploy@v0
with: with:
repoToken: "${{ secrets.GITHUB_TOKEN }}" repoToken: "${{ secrets.GITHUB_TOKEN }}"
firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

@ -1,8 +1 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
export 'view/app.dart'; export 'view/app.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'package:authentication_repository/authentication_repository.dart'; import 'package:authentication_repository/authentication_repository.dart';

@ -0,0 +1,2 @@
export 'cubit/assets_manager_cubit.dart';
export 'views/views.dart';

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template assets_loading_page}
/// Widget used to indicate the loading progress of the different assets used
/// in the game
/// {@endtemplate}
class AssetsLoadingPage extends StatelessWidget {
/// {@macro assets_loading_page}
const AssetsLoadingPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final headline1 = Theme.of(context).textTheme.headline1;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.ioPinball,
style: headline1!.copyWith(fontSize: 80),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
AnimatedEllipsisText(
l10n.loading,
style: headline1,
),
const SizedBox(height: 40),
FractionallySizedBox(
widthFactor: 0.8,
child: BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
builder: (context, state) {
return PinballLoadingIndicator(value: state.progress);
},
),
),
],
),
);
}
}

@ -0,0 +1 @@
export 'assets_loading_page.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:async'; import 'dart:async';

@ -1,6 +1,8 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -16,21 +18,33 @@ class AndroidAcres extends Component {
SpaceshipRamp(), SpaceshipRamp(),
SpaceshipRail(), SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a( AndroidBumper.a(
children: [ children: [
ScoringBehavior(points: 20000), BumperScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-25, 1.3), )..initialPosition = Vector2(-25, 1.3),
AndroidBumper.b( AndroidBumper.b(
children: [ children: [
ScoringBehavior(points: 20000), BumperScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-32.8, -9.2), )..initialPosition = Vector2(-32.8, -9.2),
AndroidBumper.cow( AndroidBumper.cow(
children: [ children: [
ScoringBehavior(points: 20), BumperScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-20.5, -13.8), )..initialPosition = Vector2(-20.5, -13.8),
AndroidSpaceshipBonusBehavior(),
], ],
); );
/// Creates [AndroidAcres] without any children.
///
/// This can be used for testing [AndroidAcres]'s behaviors in isolation.
@visibleForTesting
AndroidAcres.test();
} }

@ -0,0 +1,27 @@
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.androidSpaceship] when [AndroidSpaceship] has a bonus.
class AndroidSpaceshipBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<AndroidAcres> {
@override
void onMount() {
super.onMount();
final androidSpaceship = parent.firstChild<AndroidSpaceship>()!;
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
androidSpaceship.bloc.stream.listen((state) {
final listenWhen = state == AndroidSpaceshipState.withBonus;
if (!listenWhen) return;
gameRef
.read<GameBloc>()
.add(const BonusActivated(GameBonus.androidSpaceship));
androidSpaceship.bloc.onBonusAwarded();
});
}
}

@ -0,0 +1 @@
export 'android_spaceship_bonus_behavior.dart';

@ -51,7 +51,7 @@ class _BottomGroupSide extends Component {
final kicker = Kicker( final kicker = Kicker(
side: _side, side: _side,
children: [ children: [
ScoringBehavior(points: 5000)..applyTo(['bouncy_edge']), ScoringBehavior(points: Points.fiveThousand)..applyTo(['bouncy_edge']),
], ],
)..initialPosition = Vector2( )..initialPosition = Vector2(
(22.64 * direction) + centerXAdjustment, (22.64 * direction) + centerXAdjustment,

@ -1,15 +1,16 @@
export 'android_acres.dart'; export 'android_acres/android_acres.dart';
export 'bottom_group.dart'; export 'bottom_group.dart';
export 'camera_controller.dart'; export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
export 'controlled_plunger.dart'; export 'controlled_plunger.dart';
export 'dino_desert.dart'; export 'dino_desert/dino_desert.dart';
export 'drain.dart'; export 'drain.dart';
export 'flutter_forest/flutter_forest.dart'; export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_flow_controller.dart';
export 'google_word/google_word.dart'; export 'google_word/google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart'; export 'multipliers/multipliers.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';
export 'sparky_scorch.dart'; export 'sparky_scorch.dart';

@ -67,7 +67,9 @@ class BallController extends ComponentController<Ball>
const Duration(milliseconds: 2583), const Duration(milliseconds: 2583),
); );
component.resume(); component.resume();
await component.boost(Vector2(40, 110)); await component.add(
BallTurboChargingBehavior(impulse: Vector2(40, 110)),
);
} }
@override @override

@ -0,0 +1 @@
export 'chrome_dino_bonus_behavior.dart';

@ -0,0 +1,24 @@
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.dinoChomp] when a [Ball] is chomped by the [ChromeDino].
class ChromeDinoBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<DinoDesert> {
@override
void onMount() {
super.onMount();
final chromeDino = parent.firstChild<ChromeDino>()!;
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
chromeDino.bloc.stream.listen((state) {
final listenWhen = state.status == ChromeDinoStatus.chomping;
if (!listenWhen) return;
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.dinoChomp));
});
}
}

@ -1,11 +1,13 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template dino_desert} /// {@template dino_desert}
/// Area located next to the [Launcher] containing the [ChromeDino] and /// Area located next to the [Launcher] containing the [ChromeDino],
/// [DinoWalls]. /// [DinoWalls], and the [Slingshots].
/// {@endtemplate} /// {@endtemplate}
class DinoDesert extends Component { class DinoDesert extends Component {
/// {@macro dino_desert} /// {@macro dino_desert}
@ -14,14 +16,22 @@ class DinoDesert extends Component {
children: [ children: [
ChromeDino( ChromeDino(
children: [ children: [
ScoringBehavior(points: 200000)..applyTo(['inside_mouth']), ScoringBehavior(points: Points.twoHundredThousand)
..applyTo(['inside_mouth']),
], ],
)..initialPosition = Vector2(12.6, -6.9), )..initialPosition = Vector2(12.6, -6.9),
_BarrierBehindDino(), _BarrierBehindDino(),
DinoWalls(), DinoWalls(),
Slingshots(), Slingshots(),
ChromeDinoBonusBehavior(),
], ],
); );
/// Creates [DinoDesert] without any children.
///
/// This can be used for testing [DinoDesert]'s behaviors in isolation.
@visibleForTesting
DinoDesert.test();
} }
class _BarrierBehindDino extends BodyComponent { class _BarrierBehindDino extends BodyComponent {

@ -3,7 +3,10 @@ import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] /// Bonus obtained at the [FlutterForest].
///
/// When all [DashNestBumper]s are hit at least once three times, the [Signpost]
/// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball]. /// is awarded, and the [DashNestBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component class FlutterForestBonusBehavior extends Component
with ParentIsA<FlutterForest>, HasGameRef<PinballGame> { with ParentIsA<FlutterForest>, HasGameRef<PinballGame> {
@ -12,28 +15,36 @@ class FlutterForestBonusBehavior extends Component
super.onMount(); super.onMount();
final bumpers = parent.children.whereType<DashNestBumper>(); final bumpers = parent.children.whereType<DashNestBumper>();
final signpost = parent.firstChild<Signpost>()!;
final animatronic = parent.firstChild<DashAnimatronic>()!;
final canvas = gameRef.firstChild<ZCanvasComponent>()!;
for (final bumper in bumpers) { for (final bumper in bumpers) {
// TODO(alestiago): Refactor subscription management once the following is // TODO(alestiago): Refactor subscription management once the following is
// merged: // merged:
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
bumper.bloc.stream.listen((state) { bumper.bloc.stream.listen((state) {
final achievedBonus = bumpers.every( final activatedAllBumpers = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active, (bumper) => bumper.bloc.state == DashNestBumperState.active,
); );
if (achievedBonus) { if (activatedAllBumpers) {
gameRef signpost.bloc.onProgressed();
.read<GameBloc>()
.add(const BonusActivated(GameBonus.dashNest));
gameRef.firstChild<ZCanvasComponent>()!.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme)
..initialPosition = Vector2(17.2, -52.7),
);
parent.firstChild<DashAnimatronic>()?.playing = true;
for (final bumper in bumpers) { for (final bumper in bumpers) {
bumper.bloc.onReset(); bumper.bloc.onReset();
} }
if (signpost.bloc.isFullyProgressed()) {
gameRef
.read<GameBloc>()
.add(const BonusActivated(GameBonus.dashNest));
canvas.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme)
..initialPosition = Vector2(29.5, -24.5),
);
animatronic.playing = true;
signpost.bloc.onProgressed();
}
} }
}); });
} }

@ -18,22 +18,22 @@ class FlutterForest extends Component with ZIndex {
children: [ children: [
Signpost( Signpost(
children: [ children: [
ScoringBehavior(points: 20), BumperScoringBehavior(points: Points.fiveThousand),
], ],
)..initialPosition = Vector2(8.35, -58.3), )..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main( DashNestBumper.main(
children: [ children: [
ScoringBehavior(points: 200000), BumperScoringBehavior(points: Points.twoHundredThousand),
], ],
)..initialPosition = Vector2(18.55, -59.35), )..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a( DashNestBumper.a(
children: [ children: [
ScoringBehavior(points: 20000), BumperScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(8.95, -51.95), )..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b( DashNestBumper.b(
children: [ children: [
ScoringBehavior(points: 20000), BumperScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(22.3, -46.75), )..initialPosition = Vector2(22.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66), DashAnimatronic()..position = Vector2(20, -66),

@ -39,6 +39,7 @@ class GameFlowController extends ComponentController<PinballGame>
/// Puts the game on a playing state /// Puts the game on a playing state
void start() { void start() {
component.audio.backgroundMusic();
component.firstChild<Backboard>()?.waitingMode(); component.firstChild<Backboard>()?.waitingMode();
component.firstChild<CameraController>()?.focusOnGame(); component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay); component.overlays.remove(PinballGame.playButtonOverlay);

@ -16,27 +16,27 @@ class GoogleWord extends Component with ZIndex {
children: [ children: [
GoogleLetter( GoogleLetter(
0, 0,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-13.1, 1.72), )..initialPosition = position + Vector2(-13.1, 1.72),
GoogleLetter( GoogleLetter(
1, 1,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-8.33, -0.75), )..initialPosition = position + Vector2(-8.33, -0.75),
GoogleLetter( GoogleLetter(
2, 2,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-2.88, -1.85), )..initialPosition = position + Vector2(-2.88, -1.85),
GoogleLetter( GoogleLetter(
3, 3,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(2.88, -1.85), )..initialPosition = position + Vector2(2.88, -1.85),
GoogleLetter( GoogleLetter(
4, 4,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(8.33, -0.75), )..initialPosition = position + Vector2(8.33, -0.75),
GoogleLetter( GoogleLetter(
5, 5,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(13.1, 1.72), )..initialPosition = position + Vector2(13.1, 1.72),
GoogleWordBonusBehavior(), GoogleWordBonusBehavior(),
], ],

@ -12,6 +12,7 @@ class Launcher extends Component {
: super( : super(
children: [ children: [
LaunchRamp(), LaunchRamp(),
Flapper(),
ControlledPlunger(compressionDistance: 9.2) ControlledPlunger(compressionDistance: 9.2)
..initialPosition = Vector2(41.2, 43.7), ..initialPosition = Vector2(41.2, 43.7),
RocketSpriteComponent()..position = Vector2(43, 62.3), RocketSpriteComponent()..position = Vector2(43, 62.3),

@ -0,0 +1 @@
export 'multiballs_behavior.dart';

@ -0,0 +1,28 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Toggle each [Multiball] when there is a bonus ball.
class MultiballsBehavior extends Component
with
HasGameRef<PinballGame>,
ParentIsA<Multiballs>,
BlocComponent<GameBloc, GameState> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
final hasChanged = previousState?.bonusHistory != newState.bonusHistory;
final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty &&
newState.bonusHistory.last == GameBonus.dashNest;
return hasChanged && lastBonusIsMultiball;
}
@override
void onNewState(GameState state) {
parent.children.whereType<Multiball>().forEach((multiball) {
multiball.bloc.onAnimate();
});
}
}

@ -0,0 +1,30 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template multiballs_component}
/// A [SpriteGroupComponent] for the multiball over the board.
/// {@endtemplate}
class Multiballs extends Component with ZIndex {
/// {@macro multiballs_component}
Multiballs()
: super(
children: [
Multiball.a(),
Multiball.b(),
Multiball.c(),
Multiball.d(),
MultiballsBehavior(),
],
) {
zIndex = ZIndexes.decal;
}
/// Creates a [Multiballs] without any children.
///
/// This can be used for testing [Multiballs]'s behaviors in isolation.
@visibleForTesting
Multiballs.test();
}

@ -12,23 +12,42 @@ import 'package:pinball_flame/pinball_flame.dart';
class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> { class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> {
/// {@macro scoring_behavior} /// {@macro scoring_behavior}
ScoringBehavior({ ScoringBehavior({
required int points, required Points points,
}) : _points = points; }) : _points = points;
final int _points; final Points _points;
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);
if (other is! Ball) return; if (other is! Ball) return;
gameRef.read<GameBloc>().add(Scored(points: _points)); gameRef.read<GameBloc>().add(Scored(points: _points.value));
gameRef.audio.score();
gameRef.firstChild<ZCanvasComponent>()!.add( gameRef.firstChild<ZCanvasComponent>()!.add(
ScoreText( ScoreComponent(
text: _points.toString(), points: _points,
position: other.body.position, position: other.body.position,
), ),
); );
} }
} }
/// {@template bumper_scoring_behavior}
/// A specific [ScoringBehavior] used for Bumpers.
/// In addition to its parent logic, also plays the
/// SFX for bumpers
/// {@endtemplate}
class BumperScoringBehavior extends ScoringBehavior {
/// {@macro bumper_scoring_behavior}
BumperScoringBehavior({
required Points points,
}) : super(points: points);
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
gameRef.audio.bumper();
}
}

@ -16,17 +16,17 @@ class SparkyScorch extends Component {
children: [ children: [
SparkyBumper.a( SparkyBumper.a(
children: [ children: [
ScoringBehavior(points: 20000), BumperScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-22.9, -41.65), )..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b( SparkyBumper.b(
children: [ children: [
ScoringBehavior(points: 20000), BumperScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-21.25, -57.9), )..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c( SparkyBumper.c(
children: [ children: [
ScoringBehavior(points: 20000), BumperScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9), SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9),
@ -47,7 +47,7 @@ class SparkyComputerSensor extends BodyComponent
: super( : super(
renderBody: false, renderBody: false,
children: [ children: [
ScoringBehavior(points: 200000), ScoringBehavior(points: Points.twentyThousand),
], ],
); );

@ -1,4 +1,3 @@
export 'assets_manager/cubit/assets_manager_cubit.dart';
export 'bloc/game_bloc.dart'; export 'bloc/game_bloc.dart';
export 'components/components.dart'; export 'components/components.dart';
export 'game_assets.dart'; export 'game_assets.dart';

@ -1,5 +1,4 @@
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' as components; import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
@ -39,6 +38,7 @@ extension PinballGameAssetsX on PinballGame {
), ),
images.load(components.Assets.images.dino.bottomWall.keyName), images.load(components.Assets.images.dino.bottomWall.keyName),
images.load(components.Assets.images.dino.topWall.keyName), images.load(components.Assets.images.dino.topWall.keyName),
images.load(components.Assets.images.dino.topWallTunnel.keyName),
images.load(components.Assets.images.dino.animatronic.head.keyName), images.load(components.Assets.images.dino.animatronic.head.keyName),
images.load(components.Assets.images.dino.animatronic.mouth.keyName), images.load(components.Assets.images.dino.animatronic.mouth.keyName),
images.load(components.Assets.images.dash.animatronic.keyName), images.load(components.Assets.images.dash.animatronic.keyName),
@ -114,6 +114,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter6.lit.keyName), images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName), images.load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.backboard.display.keyName), images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiball.lit.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName),
images.load(components.Assets.images.multiplier.x2.dimmed.keyName), images.load(components.Assets.images.multiplier.x2.dimmed.keyName),
images.load(components.Assets.images.multiplier.x3.lit.keyName), images.load(components.Assets.images.multiplier.x3.lit.keyName),
@ -124,11 +126,17 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.multiplier.x5.dimmed.keyName), images.load(components.Assets.images.multiplier.x5.dimmed.keyName),
images.load(components.Assets.images.multiplier.x6.lit.keyName), images.load(components.Assets.images.multiplier.x6.lit.keyName),
images.load(components.Assets.images.multiplier.x6.dimmed.keyName), images.load(components.Assets.images.multiplier.x6.dimmed.keyName),
images.load(components.Assets.images.score.fiveThousand.keyName),
images.load(components.Assets.images.score.twentyThousand.keyName),
images.load(components.Assets.images.score.twoHundredThousand.keyName),
images.load(components.Assets.images.score.oneMillion.keyName),
images.load(components.Assets.images.flapper.backSupport.keyName),
images.load(components.Assets.images.flapper.frontSupport.keyName),
images.load(components.Assets.images.flapper.flap.keyName),
images.load(dashTheme.leaderboardIcon.keyName), images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName),
images.load(dinoTheme.leaderboardIcon.keyName), images.load(dinoTheme.leaderboardIcon.keyName),
images.load(Assets.images.components.background.path),
]; ];
} }
} }

@ -5,7 +5,6 @@ import 'package:flame/components.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
@ -14,12 +13,12 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends Forge2DGame class PinballGame extends PinballForge2DGame
with with
FlameBloc, FlameBloc,
HasKeyboardHandlerComponents, HasKeyboardHandlerComponents,
Controls<_GameBallsController>, Controls<_GameBallsController>,
TapDetector { MultiTouchTapDetector {
PinballGame({ PinballGame({
required this.characterTheme, required this.characterTheme,
required this.audio, required this.audio,
@ -53,6 +52,7 @@ class PinballGame extends Forge2DGame
final decals = [ final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)), GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(), Multipliers(),
Multiballs(),
]; ];
final characterAreas = [ final characterAreas = [
AndroidAcres(), AndroidAcres(),
@ -80,14 +80,14 @@ class PinballGame extends Forge2DGame
BoardSide? focusedBoardSide; BoardSide? focusedBoardSide;
@override @override
void onTapDown(TapDownInfo info) { void onTapDown(int pointerId, TapDownInfo info) {
if (info.raw.kind == PointerDeviceKind.touch) { if (info.raw.kind == PointerDeviceKind.touch) {
final rocket = descendants().whereType<RocketSpriteComponent>().first; final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size; final bounds = rocket.topLeftPosition & rocket.size;
// NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually.
if (bounds.contains(info.eventPosition.game.toOffset())) { if (bounds.contains(info.eventPosition.game.toOffset())) {
descendants().whereType<Plunger>().single.pull(); descendants().whereType<Plunger>().single.pullFor(2);
} else { } else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; final leftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right;
@ -98,28 +98,19 @@ class PinballGame extends Forge2DGame
} }
} }
super.onTapDown(info); super.onTapDown(pointerId, info);
} }
@override @override
void onTapUp(TapUpInfo info) { void onTapUp(int pointerId, TapUpInfo info) {
final rocket = descendants().whereType<RocketSpriteComponent>().first; _moveFlippersDown();
final bounds = rocket.topLeftPosition & rocket.size; super.onTapUp(pointerId, info);
if (bounds.contains(info.eventPosition.game.toOffset())) {
descendants().whereType<Plunger>().single.release();
} else {
_moveFlippersDown();
}
super.onTapUp(info);
} }
@override @override
void onTapCancel() { void onTapCancel(int pointerId) {
descendants().whereType<Plunger>().single.release();
_moveFlippersDown(); _moveFlippersDown();
super.onTapCancel(); super.onTapCancel(pointerId);
} }
void _moveFlippersDown() { void _moveFlippersDown() {
@ -190,8 +181,8 @@ class DebugPinballGame extends PinballGame with FPSCounter {
} }
@override @override
void onTapUp(TapUpInfo info) { void onTapUp(int pointerId, TapUpInfo info) {
super.onTapUp(info); super.onTapUp(pointerId, info);
if (info.raw.kind == PointerDeviceKind.mouse) { if (info.raw.kind == PointerDeviceKind.mouse) {
final ball = ControlledBall.debug() final ball = ControlledBall.debug()

@ -4,10 +4,12 @@ import 'package:flame/game.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
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/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
class PinballGamePage extends StatelessWidget { class PinballGamePage extends StatelessWidget {
const PinballGamePage({ const PinballGamePage({
@ -44,15 +46,14 @@ class PinballGamePage extends StatelessWidget {
...game.preLoadAssets(), ...game.preLoadAssets(),
pinballAudio.load(), pinballAudio.load(),
...BonusAnimation.loadAssets(), ...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
]; ];
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (_) => StartGameBloc(game: game)), BlocProvider(create: (_) => StartGameBloc(game: game)),
BlocProvider(create: (_) => GameBloc()), BlocProvider(create: (_) => GameBloc()),
BlocProvider( BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()),
create: (_) => AssetsManagerCubit(loadables)..load(),
),
], ],
child: PinballGameView(game: game), child: PinballGameView(game: game),
); );
@ -72,32 +73,13 @@ class PinballGameView extends StatelessWidget {
final isLoading = context.select( final isLoading = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress != 1, (AssetsManagerCubit bloc) => bloc.state.progress != 1,
); );
return Container(
return Scaffold( decoration: const CrtBackground(),
backgroundColor: Colors.blue, child: Scaffold(
body: isLoading backgroundColor: PinballColors.transparent,
? const _PinballGameLoadingView() body: isLoading
: PinballGameLoadedView(game: game), ? const AssetsLoadingPage()
); : PinballGameLoadedView(game: game),
}
}
class _PinballGameLoadingView extends StatelessWidget {
const _PinballGameLoadingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final loadingProgress = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress,
);
return Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: LinearProgressIndicator(
color: Colors.white,
value: loadingProgress,
),
), ),
); );
} }

@ -126,7 +126,7 @@ class _BonusAnimationState extends State<BonusAnimation>
); );
animation = spriteSheet.createAnimation( animation = spriteSheet.createAnimation(
row: 0, row: 0,
stepTime: 1 / 24, stepTime: 1 / 12,
to: spriteSheet.rows * spriteSheet.columns, to: spriteSheet.rows * spriteSheet.columns,
loop: false, loop: false,
); );

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pinball/game/pinball_game.dart'; import 'package:pinball/game/pinball_game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template play_button_overlay} /// {@template play_button_overlay}
/// [Widget] that renders the button responsible to starting the game /// [Widget] that renders the button responsible to starting the game
@ -20,14 +21,12 @@ class PlayButtonOverlay extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return Center( return PinballButton(
child: ElevatedButton( text: l10n.play,
onPressed: () async { onTap: () async {
_game.gameFlowController.start(); _game.gameFlowController.start();
await showCharacterSelectionDialog(context); await showCharacterSelectionDialog(context);
}, },
child: Text(l10n.play),
),
); );
} }
} }

@ -44,10 +44,6 @@ class $AssetsImagesBonusAnimationGen {
class $AssetsImagesComponentsGen { class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
/// File path: assets/images/components/background.png
AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png');
/// File path: assets/images/components/key.png /// File path: assets/images/components/key.png
AssetGenImage get key => AssetGenImage get key =>
const AssetGenImage('assets/images/components/key.png'); const AssetGenImage('assets/images/components/key.png');

@ -3,8 +3,10 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/gen/gen.dart'; import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
import 'package:platform_helper/platform_helper.dart'; import 'package:platform_helper/platform_helper.dart';
@ -50,11 +52,13 @@ extension on Control {
} }
Future<void> showHowToPlayDialog(BuildContext context) { Future<void> showHowToPlayDialog(BuildContext context) {
final audio = context.read<PinballAudio>();
return showDialog<void>( return showDialog<void>(
context: context, context: context,
barrierDismissible: false,
builder: (_) => HowToPlayDialog(), builder: (_) => HowToPlayDialog(),
); ).then((_) {
audio.ioPinballVoiceOver();
});
} }
class HowToPlayDialog extends StatefulWidget { class HowToPlayDialog extends StatefulWidget {

@ -20,7 +20,7 @@
"@flipperControls": { "@flipperControls": {
"description": "Text displayed on the how to play dialog with the flipper controls" "description": "Text displayed on the how to play dialog with the flipper controls"
}, },
"tapAndHoldRocket": "Tap & Hold Rocket", "tapAndHoldRocket": "Tap Rocket",
"@tapAndHoldRocket": { "@tapAndHoldRocket": {
"description": "Text displayed on the how to launch on mobile" "description": "Text displayed on the how to launch on mobile"
}, },
@ -123,5 +123,13 @@
"footerGoogleIOText": "Google I/O", "footerGoogleIOText": "Google I/O",
"@footerGoogleIOText": { "@footerGoogleIOText": {
"description": "Text shown on the footer which mentions Google I/O" "description": "Text shown on the footer which mentions Google I/O"
},
"loading": "Loading",
"@loading": {
"description": "Text shown to indicate loading times"
},
"ioPinball": "I/O Pinball",
"@ioPinball": {
"description": "I/O Pinball - Name of the game"
} }
} }

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async'; import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart'; import 'package:authentication_repository/authentication_repository.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async'; import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart'; import 'package:authentication_repository/authentication_repository.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async'; import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart'; import 'package:authentication_repository/authentication_repository.dart';

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/l10n/l10n.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/select_character/select_character.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -118,19 +117,7 @@ class _CharacterPreview extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<CharacterThemeCubit, CharacterThemeState>( return BlocBuilder<CharacterThemeCubit, CharacterThemeState>(
builder: (context, state) { builder: (context, state) {
return Column( return SelectedCharacter(currentCharacter: state.characterTheme);
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()),
],
);
}, },
); );
} }
@ -151,8 +138,8 @@ class _Character extends StatelessWidget {
return Expanded( return Expanded(
child: Opacity( child: Opacity(
opacity: isSelected ? 1 : 0.3, opacity: isSelected ? 1 : 0.3,
child: InkWell( child: TextButton(
onTap: () => onPressed: () =>
context.read<CharacterThemeCubit>().characterSelected(character), context.read<CharacterThemeCubit>().characterSelected(character),
child: character.icon.image(fit: BoxFit.contain), child: character.icon.image(fit: BoxFit.contain),
), ),

@ -0,0 +1,102 @@
import 'package:flame/components.dart';
import 'package:flame/flame.dart';
import 'package:flame/sprite.dart';
import 'package:flutter/material.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template selected_character}
/// Shows an animated version of the character currently selected.
/// {@endtemplate}
class SelectedCharacter extends StatefulWidget {
/// {@macro selected_character}
const SelectedCharacter({
Key? key,
required this.currentCharacter,
}) : super(key: key);
/// The character that is selected at the moment.
final CharacterTheme currentCharacter;
@override
State<SelectedCharacter> createState() => _SelectedCharacterState();
/// Returns a list of assets to be loaded.
static List<Future> loadAssets() {
return [
Flame.images.load(const DashTheme().animation.keyName),
Flame.images.load(const AndroidTheme().animation.keyName),
Flame.images.load(const DinoTheme().animation.keyName),
Flame.images.load(const SparkyTheme().animation.keyName),
];
}
}
class _SelectedCharacterState extends State<SelectedCharacter>
with TickerProviderStateMixin {
SpriteAnimationController? _controller;
@override
void initState() {
super.initState();
_setupCharacterAnimation();
}
@override
void didUpdateWidget(covariant SelectedCharacter oldWidget) {
super.didUpdateWidget(oldWidget);
_setupCharacterAnimation();
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
widget.currentCharacter.name,
style: Theme.of(context).textTheme.headline2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
width: constraints.maxWidth,
height: constraints.maxHeight,
child: SpriteAnimationWidget(
controller: _controller!,
anchor: Anchor.center,
),
);
},
),
),
],
);
}
void _setupCharacterAnimation() {
final spriteSheet = SpriteSheet.fromColumnsAndRows(
image: Flame.images.fromCache(widget.currentCharacter.animation.keyName),
columns: 12,
rows: 6,
);
final animation = spriteSheet.createAnimation(
row: 0,
stepTime: 1 / 24,
to: spriteSheet.rows * spriteSheet.columns,
);
if (_controller != null) _controller?.dispose();
_controller = SpriteAnimationController(vsync: this, animation: animation)
..forward()
..repeat();
}
}

@ -1 +1,2 @@
export 'character_selection_page.dart'; export 'character_selection_page.dart';
export 'selected_character.dart';

@ -3,9 +3,9 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
class MockFirebaseAuth extends Mock implements FirebaseAuth {} class _MockFirebaseAuth extends Mock implements FirebaseAuth {}
class MockUserCredential extends Mock implements UserCredential {} class _MockUserCredential extends Mock implements UserCredential {}
void main() { void main() {
late FirebaseAuth firebaseAuth; late FirebaseAuth firebaseAuth;
@ -14,8 +14,8 @@ void main() {
group('AuthenticationRepository', () { group('AuthenticationRepository', () {
setUp(() { setUp(() {
firebaseAuth = MockFirebaseAuth(); firebaseAuth = _MockFirebaseAuth();
userCredential = MockUserCredential(); userCredential = _MockUserCredential();
authenticationRepository = AuthenticationRepository(firebaseAuth); authenticationRepository = AuthenticationRepository(firebaseAuth);
}); });

@ -5,23 +5,23 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
class MockFirebaseFirestore extends Mock implements FirebaseFirestore {} class _MockFirebaseFirestore extends Mock implements FirebaseFirestore {}
class MockCollectionReference extends Mock class _MockCollectionReference extends Mock
implements CollectionReference<Map<String, dynamic>> {} implements CollectionReference<Map<String, dynamic>> {}
class MockQuery extends Mock implements Query<Map<String, dynamic>> {} class _MockQuery extends Mock implements Query<Map<String, dynamic>> {}
class MockQuerySnapshot extends Mock class _MockQuerySnapshot extends Mock
implements QuerySnapshot<Map<String, dynamic>> {} implements QuerySnapshot<Map<String, dynamic>> {}
class MockQueryDocumentSnapshot extends Mock class _MockQueryDocumentSnapshot extends Mock
implements QueryDocumentSnapshot<Map<String, dynamic>> {} implements QueryDocumentSnapshot<Map<String, dynamic>> {}
class MockDocumentReference extends Mock class _MockDocumentReference extends Mock
implements DocumentReference<Map<String, dynamic>> {} implements DocumentReference<Map<String, dynamic>> {}
class MockDocumentSnapshot extends Mock class _MockDocumentSnapshot extends Mock
implements DocumentSnapshot<Map<String, dynamic>> {} implements DocumentSnapshot<Map<String, dynamic>> {}
void main() { void main() {
@ -29,7 +29,7 @@ void main() {
late FirebaseFirestore firestore; late FirebaseFirestore firestore;
setUp(() { setUp(() {
firestore = MockFirebaseFirestore(); firestore = _MockFirebaseFirestore();
}); });
test('can be instantiated', () { test('can be instantiated', () {
@ -70,11 +70,11 @@ void main() {
setUp(() { setUp(() {
leaderboardRepository = LeaderboardRepository(firestore); leaderboardRepository = LeaderboardRepository(firestore);
collectionReference = MockCollectionReference(); collectionReference = _MockCollectionReference();
query = MockQuery(); query = _MockQuery();
querySnapshot = MockQuerySnapshot(); querySnapshot = _MockQuerySnapshot();
queryDocumentSnapshots = top10Scores.map((score) { queryDocumentSnapshots = top10Scores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'user$score', 'playerInitials': 'user$score',
@ -119,7 +119,7 @@ void main() {
'playerInitials': 'ABC', 'playerInitials': 'ABC',
'score': 1500, 'score': 1500,
}; };
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(() => querySnapshot.docs).thenReturn([queryDocumentSnapshot]); when(() => querySnapshot.docs).thenReturn([queryDocumentSnapshot]);
when(queryDocumentSnapshot.data) when(queryDocumentSnapshot.data)
.thenReturn(top10LeaderboardDataMalformed); .thenReturn(top10LeaderboardDataMalformed);
@ -156,12 +156,12 @@ void main() {
setUp(() { setUp(() {
leaderboardRepository = LeaderboardRepository(firestore); leaderboardRepository = LeaderboardRepository(firestore);
collectionReference = MockCollectionReference(); collectionReference = _MockCollectionReference();
documentReference = MockDocumentReference(); documentReference = _MockDocumentReference();
query = MockQuery(); query = _MockQuery();
querySnapshot = MockQuerySnapshot(); querySnapshot = _MockQuerySnapshot();
queryDocumentSnapshots = leaderboardScores.map((score) { queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -228,7 +228,7 @@ void main() {
5000 5000
]; ];
final queryDocumentSnapshots = leaderboardScores.map((score) { final queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -248,8 +248,8 @@ void main() {
test( test(
'throws DeleteLeaderboardException ' 'throws DeleteLeaderboardException '
'when deleting scores outside the top 10 fails', () async { 'when deleting scores outside the top 10 fails', () async {
final deleteQuery = MockQuery(); final deleteQuery = _MockQuery();
final deleteQuerySnapshot = MockQuerySnapshot(); final deleteQuerySnapshot = _MockQuerySnapshot();
final newScore = LeaderboardEntryData( final newScore = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 15000, score: 15000,
@ -269,7 +269,7 @@ void main() {
5000, 5000,
]; ];
final deleteDocumentSnapshots = [5500, 5000].map((score) { final deleteDocumentSnapshots = [5500, 5000].map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -284,7 +284,7 @@ void main() {
when(() => deleteQuerySnapshot.docs) when(() => deleteQuerySnapshot.docs)
.thenReturn(deleteDocumentSnapshots); .thenReturn(deleteDocumentSnapshots);
final queryDocumentSnapshots = leaderboardScores.map((score) { final queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -310,8 +310,8 @@ void main() {
'saves the new score when there are more than 10 scores in the ' 'saves the new score when there are more than 10 scores in the '
'leaderboard and the new score is higher than the lowest top 10, and ' 'leaderboard and the new score is higher than the lowest top 10, and '
'deletes the scores that are not in the top 10 anymore', () async { 'deletes the scores that are not in the top 10 anymore', () async {
final deleteQuery = MockQuery(); final deleteQuery = _MockQuery();
final deleteQuerySnapshot = MockQuerySnapshot(); final deleteQuerySnapshot = _MockQuerySnapshot();
final newScore = LeaderboardEntryData( final newScore = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 15000, score: 15000,
@ -331,7 +331,7 @@ void main() {
5000, 5000,
]; ];
final deleteDocumentSnapshots = [5500, 5000].map((score) { final deleteDocumentSnapshots = [5500, 5000].map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -346,7 +346,7 @@ void main() {
when(() => deleteQuerySnapshot.docs) when(() => deleteQuerySnapshot.docs)
.thenReturn(deleteDocumentSnapshots); .thenReturn(deleteDocumentSnapshots);
final queryDocumentSnapshots = leaderboardScores.map((score) { final queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -376,9 +376,9 @@ void main() {
late DocumentSnapshot<Map<String, dynamic>> documentSnapshot; late DocumentSnapshot<Map<String, dynamic>> documentSnapshot;
setUp(() async { setUp(() async {
collectionReference = MockCollectionReference(); collectionReference = _MockCollectionReference();
documentReference = MockDocumentReference(); documentReference = _MockDocumentReference();
documentSnapshot = MockDocumentSnapshot(); documentSnapshot = _MockDocumentSnapshot();
leaderboardRepository = LeaderboardRepository(firestore); leaderboardRepository = LeaderboardRepository(firestore);
when(() => firestore.collection('prohibitedInitials')) when(() => firestore.collection('prohibitedInitials'))

@ -5,16 +5,25 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsMusicGen {
const $AssetsMusicGen();
String get background => 'assets/music/background.mp3';
}
class $AssetsSfxGen { class $AssetsSfxGen {
const $AssetsSfxGen(); const $AssetsSfxGen();
String get google => 'assets/sfx/google.ogg'; String get bumperA => 'assets/sfx/bumper_a.mp3';
String get plim => 'assets/sfx/plim.ogg'; String get bumperB => 'assets/sfx/bumper_b.mp3';
String get google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';
} }
class Assets { class Assets {
Assets._(); Assets._();
static const $AssetsMusicGen music = $AssetsMusicGen();
static const $AssetsSfxGen sfx = $AssetsSfxGen(); static const $AssetsSfxGen sfx = $AssetsSfxGen();
} }

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
@ -17,6 +19,14 @@ typedef CreateAudioPool = Future<AudioPool> Function(
/// audio /// audio
typedef PlaySingleAudio = Future<void> Function(String); typedef PlaySingleAudio = Future<void> Function(String);
/// Function that defines the contract for looping a single
/// audio
typedef LoopSingleAudio = Future<void> Function(String);
/// Function that defines the contract for pre fetching an
/// audio
typedef PreCacheSingleAudio = Future<void> Function(String);
/// Function that defines the contract for configuring /// Function that defines the contract for configuring
/// an [AudioCache] instance /// an [AudioCache] instance
typedef ConfigureAudioCache = void Function(AudioCache); typedef ConfigureAudioCache = void Function(AudioCache);
@ -29,35 +39,63 @@ class PinballAudio {
PinballAudio({ PinballAudio({
CreateAudioPool? createAudioPool, CreateAudioPool? createAudioPool,
PlaySingleAudio? playSingleAudio, PlaySingleAudio? playSingleAudio,
LoopSingleAudio? loopSingleAudio,
PreCacheSingleAudio? preCacheSingleAudio,
ConfigureAudioCache? configureAudioCache, ConfigureAudioCache? configureAudioCache,
Random? seed,
}) : _createAudioPool = createAudioPool ?? AudioPool.create, }) : _createAudioPool = createAudioPool ?? AudioPool.create,
_playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play, _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play,
_loopSingleAudio = loopSingleAudio ?? FlameAudio.audioCache.loop,
_preCacheSingleAudio =
preCacheSingleAudio ?? FlameAudio.audioCache.load,
_configureAudioCache = configureAudioCache ?? _configureAudioCache = configureAudioCache ??
((AudioCache a) { ((AudioCache a) {
a.prefix = ''; a.prefix = '';
}); }),
_seed = seed ?? Random();
final CreateAudioPool _createAudioPool; final CreateAudioPool _createAudioPool;
final PlaySingleAudio _playSingleAudio; final PlaySingleAudio _playSingleAudio;
final LoopSingleAudio _loopSingleAudio;
final PreCacheSingleAudio _preCacheSingleAudio;
final ConfigureAudioCache _configureAudioCache; final ConfigureAudioCache _configureAudioCache;
late AudioPool _scorePool; final Random _seed;
late AudioPool _bumperAPool;
late AudioPool _bumperBPool;
/// Loads the sounds effects into the memory /// Loads the sounds effects into the memory
Future<void> load() async { Future<void> load() async {
_configureAudioCache(FlameAudio.audioCache); _configureAudioCache(FlameAudio.audioCache);
_scorePool = await _createAudioPool(
_prefixFile(Assets.sfx.plim), _bumperAPool = await _createAudioPool(
_prefixFile(Assets.sfx.bumperA),
maxPlayers: 4, maxPlayers: 4,
prefix: '', prefix: '',
); );
_bumperBPool = await _createAudioPool(
_prefixFile(Assets.sfx.bumperB),
maxPlayers: 4,
prefix: '',
);
await Future.wait([
_preCacheSingleAudio(_prefixFile(Assets.sfx.google)),
_preCacheSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)),
_preCacheSingleAudio(_prefixFile(Assets.music.background)),
]);
} }
/// Plays the basic score sound /// Plays a random bumper sfx.
void score() { void bumper() {
_scorePool.start(); (_seed.nextBool() ? _bumperAPool : _bumperBPool).start(volume: 0.6);
} }
/// Plays the google word bonus /// Plays the google word bonus
@ -65,6 +103,16 @@ class PinballAudio {
_playSingleAudio(_prefixFile(Assets.sfx.google)); _playSingleAudio(_prefixFile(Assets.sfx.google));
} }
/// Plays the I/O Pinball voice over audio.
void ioPinballVoiceOver() {
_playSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver));
}
/// Plays the background music
void backgroundMusic() {
_loopSingleAudio(_prefixFile(Assets.music.background));
}
String _prefixFile(String file) { String _prefixFile(String file) {
return 'packages/pinball_audio/$file'; return 'packages/pinball_audio/$file';
} }

@ -26,3 +26,4 @@ flutter_gen:
flutter: flutter:
assets: assets:
- assets/sfx/ - assets/sfx/
- assets/music/

@ -1,34 +0,0 @@
// ignore_for_file: one_member_abstracts
import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:mocktail/mocktail.dart';
abstract class _CreateAudioPoolStub {
Future<AudioPool> onCall(
String sound, {
bool? repeating,
int? maxPlayers,
int? minPlayers,
String? prefix,
});
}
class CreateAudioPoolStub extends Mock implements _CreateAudioPoolStub {}
abstract class _ConfigureAudioCacheStub {
void onCall(AudioCache cache);
}
class ConfigureAudioCacheStub extends Mock implements _ConfigureAudioCacheStub {
}
abstract class _PlaySingleAudioStub {
Future<void> onCall(String url);
}
class PlaySingleAudioStub extends Mock implements _PlaySingleAudioStub {}
class MockAudioPool extends Mock implements AudioPool {}
class MockAudioCache extends Mock implements AudioCache {}

@ -1,57 +1,115 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors, one_member_abstracts
import 'dart:math';
import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import '../helpers/helpers.dart'; class _MockAudioPool extends Mock implements AudioPool {}
class _MockAudioCache extends Mock implements AudioCache {}
class _MockCreateAudioPool extends Mock {
Future<AudioPool> onCall(
String sound, {
bool? repeating,
int? maxPlayers,
int? minPlayers,
String? prefix,
});
}
class _MockConfigureAudioCache extends Mock {
void onCall(AudioCache cache);
}
class _MockPlaySingleAudio extends Mock {
Future<void> onCall(String url);
}
class _MockLoopSingleAudio extends Mock {
Future<void> onCall(String url);
}
abstract class _PreCacheSingleAudio {
Future<void> onCall(String url);
}
class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {}
class _MockRandom extends Mock implements Random {}
void main() { void main() {
group('PinballAudio', () { group('PinballAudio', () {
test('can be instantiated', () { late _MockCreateAudioPool createAudioPool;
expect(PinballAudio(), isNotNull); late _MockConfigureAudioCache configureAudioCache;
}); late _MockPlaySingleAudio playSingleAudio;
late _MockLoopSingleAudio loopSingleAudio;
late CreateAudioPoolStub createAudioPool; late _PreCacheSingleAudio preCacheSingleAudio;
late ConfigureAudioCacheStub configureAudioCache; late Random seed;
late PlaySingleAudioStub playSingleAudio;
late PinballAudio audio; late PinballAudio audio;
setUpAll(() { setUpAll(() {
registerFallbackValue(MockAudioCache()); registerFallbackValue(_MockAudioCache());
}); });
setUp(() { setUp(() {
createAudioPool = CreateAudioPoolStub(); createAudioPool = _MockCreateAudioPool();
when( when(
() => createAudioPool.onCall( () => createAudioPool.onCall(
any(), any(),
maxPlayers: any(named: 'maxPlayers'), maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'), prefix: any(named: 'prefix'),
), ),
).thenAnswer((_) async => MockAudioPool()); ).thenAnswer((_) async => _MockAudioPool());
configureAudioCache = ConfigureAudioCacheStub(); configureAudioCache = _MockConfigureAudioCache();
when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); when(() => configureAudioCache.onCall(any())).thenAnswer((_) {});
playSingleAudio = PlaySingleAudioStub(); playSingleAudio = _MockPlaySingleAudio();
when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {}); when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {});
loopSingleAudio = _MockLoopSingleAudio();
when(() => loopSingleAudio.onCall(any())).thenAnswer((_) async {});
preCacheSingleAudio = _MockPreCacheSingleAudio();
when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {});
seed = _MockRandom();
audio = PinballAudio( audio = PinballAudio(
configureAudioCache: configureAudioCache.onCall, configureAudioCache: configureAudioCache.onCall,
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
loopSingleAudio: loopSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall,
seed: seed,
); );
}); });
test('can be instantiated', () {
expect(PinballAudio(), isNotNull);
});
group('load', () { group('load', () {
test('creates the score pool', () async { test('creates the bumpers pools', () async {
await audio.load(); await audio.load();
verify( verify(
() => createAudioPool.onCall( () => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.plim}', 'packages/pinball_audio/${Assets.sfx.bumperA}',
maxPlayers: 4,
prefix: '',
),
).called(1);
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.bumperB}',
maxPlayers: 4, maxPlayers: 4,
prefix: '', prefix: '',
), ),
@ -69,29 +127,78 @@ void main() {
audio = PinballAudio( audio = PinballAudio(
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall,
); );
await audio.load(); await audio.load();
expect(FlameAudio.audioCache.prefix, equals('')); expect(FlameAudio.audioCache.prefix, equals(''));
}); });
test('pre cache the assets', () async {
await audio.load();
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/google.mp3'),
).called(1);
verify(
() => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'),
).called(1);
});
}); });
group('score', () { group('bumper', () {
test('plays the score sound pool', () async { late AudioPool bumperAPool;
final audioPool = MockAudioPool(); late AudioPool bumperBPool;
when(audioPool.start).thenAnswer((_) async => () {});
setUp(() {
bumperAPool = _MockAudioPool();
when(() => bumperAPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when( when(
() => createAudioPool.onCall( () => createAudioPool.onCall(
any(), 'packages/pinball_audio/${Assets.sfx.bumperA}',
maxPlayers: any(named: 'maxPlayers'), maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'), prefix: any(named: 'prefix'),
), ),
).thenAnswer((_) async => audioPool); ).thenAnswer((_) async => bumperAPool);
await audio.load(); bumperBPool = _MockAudioPool();
audio.score(); when(() => bumperBPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.bumperB}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => bumperBPool);
});
group('when seed is true', () {
test('plays the bumper A sound pool', () async {
when(seed.nextBool).thenReturn(true);
await audio.load();
audio.bumper();
verify(() => bumperAPool.start(volume: 0.6)).called(1);
});
});
verify(audioPool.start).called(1); group('when seed is false', () {
test('plays the bumper B sound pool', () async {
when(seed.nextBool).thenReturn(false);
await audio.load();
audio.bumper();
verify(() => bumperBPool.start(volume: 0.6)).called(1);
});
}); });
}); });
@ -106,5 +213,30 @@ void main() {
).called(1); ).called(1);
}); });
}); });
group('ioPinballVoiceOver', () {
test('plays the correct file', () async {
await audio.load();
audio.ioPinballVoiceOver();
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.ioPinballVoiceOver}',
),
).called(1);
});
});
group('backgroundMusic', () {
test('plays the correct file', () async {
await audio.load();
audio.backgroundMusic();
verify(
() => loopSingleAudio
.onCall('packages/pinball_audio/${Assets.music.background}'),
).called(1);
});
});
}); });
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 45 KiB

@ -22,15 +22,18 @@ class $AssetsImagesGen {
$AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen();
$AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDashGen get dash => const $AssetsImagesDashGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
$AssetsImagesFlapperGen get flapper => const $AssetsImagesFlapperGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
$AssetsImagesGoogleWordGen get googleWord => $AssetsImagesGoogleWordGen get googleWord =>
const $AssetsImagesGoogleWordGen(); const $AssetsImagesGoogleWordGen();
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp => $AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
$AssetsImagesMultiballGen get multiball => const $AssetsImagesMultiballGen();
$AssetsImagesMultiplierGen get multiplier => $AssetsImagesMultiplierGen get multiplier =>
const $AssetsImagesMultiplierGen(); const $AssetsImagesMultiplierGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
@ -122,11 +125,31 @@ class $AssetsImagesDinoGen {
AssetGenImage get bottomWall => AssetGenImage get bottomWall =>
const AssetGenImage('assets/images/dino/bottom-wall.png'); const AssetGenImage('assets/images/dino/bottom-wall.png');
/// File path: assets/images/dino/top-wall-tunnel.png
AssetGenImage get topWallTunnel =>
const AssetGenImage('assets/images/dino/top-wall-tunnel.png');
/// File path: assets/images/dino/top-wall.png /// File path: assets/images/dino/top-wall.png
AssetGenImage get topWall => AssetGenImage get topWall =>
const AssetGenImage('assets/images/dino/top-wall.png'); const AssetGenImage('assets/images/dino/top-wall.png');
} }
class $AssetsImagesFlapperGen {
const $AssetsImagesFlapperGen();
/// File path: assets/images/flapper/back-support.png
AssetGenImage get backSupport =>
const AssetGenImage('assets/images/flapper/back-support.png');
/// File path: assets/images/flapper/flap.png
AssetGenImage get flap =>
const AssetGenImage('assets/images/flapper/flap.png');
/// File path: assets/images/flapper/front-support.png
AssetGenImage get frontSupport =>
const AssetGenImage('assets/images/flapper/front-support.png');
}
class $AssetsImagesFlipperGen { class $AssetsImagesFlipperGen {
const $AssetsImagesFlipperGen(); const $AssetsImagesFlipperGen();
@ -179,6 +202,18 @@ class $AssetsImagesLaunchRampGen {
const AssetGenImage('assets/images/launch_ramp/ramp.png'); const AssetGenImage('assets/images/launch_ramp/ramp.png');
} }
class $AssetsImagesMultiballGen {
const $AssetsImagesMultiballGen();
/// File path: assets/images/multiball/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiball/dimmed.png');
/// File path: assets/images/multiball/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiball/lit.png');
}
class $AssetsImagesMultiplierGen { class $AssetsImagesMultiplierGen {
const $AssetsImagesMultiplierGen(); const $AssetsImagesMultiplierGen();
@ -201,6 +236,26 @@ class $AssetsImagesPlungerGen {
const AssetGenImage('assets/images/plunger/rocket.png'); const AssetGenImage('assets/images/plunger/rocket.png');
} }
class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen();
/// File path: assets/images/score/five-thousand.png
AssetGenImage get fiveThousand =>
const AssetGenImage('assets/images/score/five-thousand.png');
/// File path: assets/images/score/one-million.png
AssetGenImage get oneMillion =>
const AssetGenImage('assets/images/score/one-million.png');
/// File path: assets/images/score/twenty-thousand.png
AssetGenImage get twentyThousand =>
const AssetGenImage('assets/images/score/twenty-thousand.png');
/// File path: assets/images/score/two-hundred-thousand.png
AssetGenImage get twoHundredThousand =>
const AssetGenImage('assets/images/score/two-hundred-thousand.png');
}
class $AssetsImagesSignpostGen { class $AssetsImagesSignpostGen {
const $AssetsImagesSignpostGen(); const $AssetsImagesSignpostGen();

@ -0,0 +1,71 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template android_animatronic}
/// Animated Android that sits on top of the [AndroidSpaceship].
/// {@endtemplate}
class AndroidAnimatronic extends BodyComponent
with InitialPosition, Layered, ZIndex {
/// {@macro android_animatronic}
AndroidAnimatronic({Iterable<Component>? children})
: super(
children: [
_AndroidAnimatronicSpriteAnimationComponent(),
...?children,
],
renderBody: false,
) {
layer = Layer.spaceship;
zIndex = ZIndexes.androidHead;
}
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 3.1,
minorRadius: 2,
)..rotate(1.4);
final bodyDef = BodyDef(position: initialPosition);
return world.createBody(bodyDef)..createFixtureFromShape(shape);
}
}
class _AndroidAnimatronicSpriteAnimationComponent
extends SpriteAnimationComponent with HasGameRef {
_AndroidAnimatronicSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(-0.24, -2.6),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.android.spaceship.animatronic.keyName,
);
const amountPerRow = 18;
const amountPerColumn = 4;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
),
);
}
}

@ -5,17 +5,25 @@ import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_spaceship_cubit.dart';
class AndroidSpaceship extends Component { class AndroidSpaceship extends Component {
AndroidSpaceship({required Vector2 position}) AndroidSpaceship({
: super( required Vector2 position,
}) : bloc = AndroidSpaceshipCubit(),
super(
children: [ children: [
_SpaceshipSaucer()..initialPosition = position, _SpaceshipSaucer()..initialPosition = position,
_SpaceshipSaucerSpriteAnimationComponent()..position = position, _SpaceshipSaucerSpriteAnimationComponent()..position = position,
_LightBeamSpriteComponent()..position = position + Vector2(2.5, 5), _LightBeamSpriteComponent()..position = position + Vector2(2.5, 5),
_AndroidHead()..initialPosition = position + Vector2(0.5, 0.25), AndroidSpaceshipEntrance(
children: [AndroidSpaceshipEntranceBallContactBehavior()],
),
_SpaceshipHole( _SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail, outsideLayer: Layer.spaceshipExitRail,
outsidePriority: ZIndexes.ballOnSpaceshipRail, outsidePriority: ZIndexes.ballOnSpaceshipRail,
@ -26,6 +34,27 @@ class AndroidSpaceship extends Component {
)..initialPosition = position - Vector2(-7.5, -1.1), )..initialPosition = position - Vector2(-7.5, -1.1),
], ],
); );
/// Creates an [AndroidSpaceship] without any children.
///
/// This can be used for testing [AndroidSpaceship]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
AndroidSpaceship.test({
required this.bloc,
Iterable<Component>? children,
}) : super(children: children);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
final AndroidSpaceshipCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
} }
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
@ -123,62 +152,32 @@ class _LightBeamSpriteComponent extends SpriteComponent
} }
} }
class _AndroidHead extends BodyComponent with InitialPosition, Layered, ZIndex { class AndroidSpaceshipEntrance extends BodyComponent
_AndroidHead() with ParentIsA<AndroidSpaceship>, Layered {
AndroidSpaceshipEntrance({Iterable<Component>? children})
: super( : super(
children: [_AndroidHeadSpriteAnimationComponent()], children: children,
renderBody: false, renderBody: false,
) { ) {
layer = Layer.spaceship; layer = Layer.spaceship;
zIndex = ZIndexes.androidHead;
} }
@override @override
Body createBody() { Body createBody() {
final shape = EllipseShape( final shape = PolygonShape()
center: Vector2.zero(), ..setAsBox(
majorRadius: 3.1, 2,
minorRadius: 2, 0.1,
)..rotate(1.4); Vector2(-27.4, -37.2),
final bodyDef = BodyDef(position: initialPosition); -0.12,
);
return world.createBody(bodyDef)..createFixtureFromShape(shape); final fixtureDef = FixtureDef(
} shape,
} isSensor: true,
class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
_AndroidHeadSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(-0.24, -2.6),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.android.spaceship.animatronic.keyName,
); );
final bodyDef = BodyDef();
const amountPerRow = 18; return world.createBody(bodyDef)..createFixture(fixtureDef);
const amountPerColumn = 4;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
),
);
} }
} }

@ -0,0 +1,16 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class AndroidSpaceshipEntranceBallContactBehavior
extends ContactBehavior<AndroidSpaceshipEntrance> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.parent.bloc.onBallEntered();
}
}

@ -0,0 +1 @@
export 'android_spaceship_entrance_ball_contact_behavior.dart.dart';

@ -0,0 +1,13 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'android_spaceship_state.dart';
class AndroidSpaceshipCubit extends Cubit<AndroidSpaceshipState> {
AndroidSpaceshipCubit() : super(AndroidSpaceshipState.withoutBonus);
void onBallEntered() => emit(AndroidSpaceshipState.withBonus);
void onBonusAwarded() => emit(AndroidSpaceshipState.withoutBonus);
}

@ -0,0 +1,8 @@
// ignore_for_file: public_member_api_docs
part of 'android_spaceship_cubit.dart';
enum AndroidSpaceshipState {
withoutBonus,
withBonus,
}

@ -1,186 +0,0 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/widgets.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around.
/// {@endtemplate}
class Ball<T extends Forge2DGame> extends BodyComponent<T>
with Layered, InitialPosition, ZIndex {
/// {@macro ball}
Ball({
required this.baseColor,
}) : super(
renderBody: false,
children: [
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
],
) {
// TODO(ruimiguel): while developing Ball can be launched by clicking mouse,
// and default layer is Layer.all. But on final game Ball will be always be
// be launched from Plunger and LauncherRamp will modify it to Layer.board.
// We need to see what happens if Ball appears from other place like nest
// bumper, it will need to explicit change layer to Layer.board then.
layer = Layer.board;
}
/// The size of the [Ball].
static final Vector2 size = Vector2.all(4.13);
/// The base [Color] used to tint this [Ball].
final Color baseColor;
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(
shape,
density: 1,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
type: BodyType.dynamic,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
/// Immediatly and completly [stop]s the ball.
///
/// The [Ball] will no longer be affected by any forces, including it's
/// weight and those emitted from collisions.
// TODO(allisonryan0002): prevent motion from contact with other balls.
void stop() {
body
..gravityScale = Vector2.zero()
..linearVelocity = Vector2.zero()
..angularVelocity = 0;
}
/// Allows the [Ball] to be affected by forces.
///
/// If previously [stop]ped, the previous ball's velocity is not kept.
void resume() {
body.gravityScale = Vector2(1, 1);
}
/// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball].
Future<void> boost(Vector2 impulse) async {
body.linearVelocity = impulse;
await add(_TurboChargeSpriteAnimationComponent());
}
@override
void update(double dt) {
super.update(dt);
_rescaleSize();
_setPositionalGravity();
}
void _rescaleSize() {
final boardHeight = BoardDimensions.bounds.height;
const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
final standardizedYPosition = body.position.y + (boardHeight / 2);
final scaleFactor = maxShrinkValue +
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor;
// TODO(alestiago): Revisit and see if there's a better way to do this.
final spriteComponent = firstChild<_BallSpriteComponent>();
spriteComponent?.scale = Vector2.all(scaleFactor);
}
void _setPositionalGravity() {
final defaultGravity = gameRef.world.gravity.y;
final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2;
const maxXGravityPercentage =
(1 - BoardDimensions.perspectiveShrinkFactor) / 2;
final xDeviationFromCenter = body.position.x;
final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) *
maxXGravityPercentage) *
defaultGravity;
final positionalYForce = math.sqrt(
math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2),
);
body.gravityOverride = Vector2(positionalXForce, positionalYForce);
}
}
class _BallSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.ball.ball.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
}
}
class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef, ZIndex {
_TurboChargeSpriteAnimationComponent()
: super(
anchor: const Anchor(0.53, 0.72),
removeOnFinish: true,
) {
zIndex = ZIndexes.turboChargeFlame;
}
late final Vector2 _textureSize;
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.ball.flameEffect.keyName,
);
const amountPerRow = 8;
const amountPerColumn = 4;
_textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: _textureSize,
loop: false,
),
);
}
@override
void update(double dt) {
super.update(dt);
if (parent != null) {
final body = (parent! as BodyComponent).body;
final direction = -body.linearVelocity.normalized();
angle = math.atan2(direction.x, -direction.y);
size = (_textureSize / 45) * body.fixtures.first.shape.radius;
}
}
}

@ -0,0 +1,96 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/widgets.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'behaviors/behaviors.dart';
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around.
/// {@endtemplate}
class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// {@macro ball}
Ball({
required this.baseColor,
}) : super(
renderBody: false,
children: [
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
BallScalingBehavior(),
BallGravitatingBehavior(),
],
) {
// TODO(ruimiguel): while developing Ball can be launched by clicking mouse,
// and default layer is Layer.all. But on final game Ball will be always be
// be launched from Plunger and LauncherRamp will modify it to Layer.board.
// We need to see what happens if Ball appears from other place like nest
// bumper, it will need to explicit change layer to Layer.board then.
layer = Layer.board;
}
/// Creates a [Ball] without any behaviors.
///
/// This can be used for testing [Ball]'s behaviors in isolation.
@visibleForTesting
Ball.test({required this.baseColor})
: super(
children: [_BallSpriteComponent()],
);
/// The size of the [Ball].
static final Vector2 size = Vector2.all(4.13);
/// The base [Color] used to tint this [Ball].
final Color baseColor;
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(
shape,
density: 1,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
type: BodyType.dynamic,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
/// Immediatly and completly [stop]s the ball.
///
/// The [Ball] will no longer be affected by any forces, including it's
/// weight and those emitted from collisions.
// TODO(allisonryan0002): prevent motion from contact with other balls.
void stop() {
body
..gravityScale = Vector2.zero()
..linearVelocity = Vector2.zero()
..angularVelocity = 0;
}
/// Allows the [Ball] to be affected by forces.
///
/// If previously [stop]ped, the previous ball's velocity is not kept.
void resume() {
body.gravityScale = Vector2(1, 1);
}
}
class _BallSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.ball.ball.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
}
}

@ -0,0 +1,35 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Scales the ball's gravity according to its position on the board.
class BallGravitatingBehavior extends Component
with ParentIsA<Ball>, HasGameRef<Forge2DGame> {
@override
void update(double dt) {
super.update(dt);
final defaultGravity = gameRef.world.gravity.y;
final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2;
const maxXGravityPercentage =
(1 - BoardDimensions.perspectiveShrinkFactor) / 2;
final xDeviationFromCenter = parent.body.position.x;
final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) *
maxXGravityPercentage) *
defaultGravity;
final positionalYForce = math.sqrt(
math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2),
);
final gravityOverride = parent.body.gravityOverride;
if (gravityOverride != null) {
gravityOverride.setValues(positionalXForce, positionalYForce);
} else {
parent.body.gravityOverride = Vector2(positionalXForce, positionalYForce);
}
}
}

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Scales the ball's body and sprite according to its position on the board.
class BallScalingBehavior extends Component with ParentIsA<Ball> {
@override
void update(double dt) {
super.update(dt);
final boardHeight = BoardDimensions.bounds.height;
const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
final standardizedYPosition = parent.body.position.y + (boardHeight / 2);
final scaleFactor = maxShrinkValue +
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor;
parent.firstChild<SpriteComponent>()!.scale.setValues(
scaleFactor,
scaleFactor,
);
}
}

@ -0,0 +1,81 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ball_turbo_charging_behavior}
/// Puts the [Ball] in flames and [_impulse]s it.
/// {@endtemplate}
class BallTurboChargingBehavior extends TimerComponent with ParentIsA<Ball> {
/// {@macro ball_turbo_charging_behavior}
BallTurboChargingBehavior({
required Vector2 impulse,
}) : _impulse = impulse,
super(period: 5, removeOnFinish: true);
final Vector2 _impulse;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.body.linearVelocity = _impulse;
await parent.add(_TurboChargeSpriteAnimationComponent());
}
@override
void onRemove() {
parent
.firstChild<_TurboChargeSpriteAnimationComponent>()!
.removeFromParent();
super.onRemove();
}
}
class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef, ZIndex, ParentIsA<Ball> {
_TurboChargeSpriteAnimationComponent()
: super(
anchor: const Anchor(0.53, 0.72),
) {
zIndex = ZIndexes.turboChargeFlame;
}
late final Vector2 _textureSize;
@override
void update(double dt) {
super.update(dt);
final direction = -parent.body.linearVelocity.normalized();
angle = math.atan2(direction.x, -direction.y);
size = (_textureSize / 45) * parent.body.fixtures.first.shape.radius;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.ball.flameEffect.keyName,
);
const amountPerRow = 8;
const amountPerColumn = 4;
_textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: _textureSize,
),
);
}
}

@ -0,0 +1,3 @@
export 'ball_gravitating_behavior.dart';
export 'ball_scaling_behavior.dart';
export 'ball_turbo_charging_behavior.dart';

@ -18,10 +18,17 @@ class ChromeDinoCubit extends Cubit<ChromeDinoState> {
} }
void onChomp(Ball ball) { void onChomp(Ball ball) {
emit(state.copyWith(status: ChromeDinoStatus.chomping, ball: ball)); if (ball != state.ball) {
emit(state.copyWith(status: ChromeDinoStatus.chomping, ball: ball));
}
} }
void onSpit() { void onSpit() {
emit(state.copyWith(status: ChromeDinoStatus.idle)); emit(
ChromeDinoState(
status: ChromeDinoStatus.idle,
isMouthOpen: state.isMouthOpen,
),
);
} }
} }

@ -1,7 +1,8 @@
export 'android_animatronic.dart';
export 'android_bumper/android_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'android_spaceship.dart'; export 'android_spaceship/android_spaceship.dart';
export 'backboard/backboard.dart'; export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball/ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board_background_sprite_component.dart'; export 'board_background_sprite_component.dart';
export 'board_dimensions.dart'; export 'board_dimensions.dart';
@ -13,6 +14,7 @@ export 'dash_animatronic.dart';
export 'dash_nest_bumper/dash_nest_bumper.dart'; export 'dash_nest_bumper/dash_nest_bumper.dart';
export 'dino_walls.dart'; export 'dino_walls.dart';
export 'fire_effect.dart'; export 'fire_effect.dart';
export 'flapper/flapper.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'google_letter/google_letter.dart'; export 'google_letter/google_letter.dart';
export 'initial_position.dart'; export 'initial_position.dart';
@ -21,12 +23,13 @@ export 'kicker/kicker.dart';
export 'launch_ramp.dart'; export 'launch_ramp.dart';
export 'layer.dart'; export 'layer.dart';
export 'layer_sensor.dart'; export 'layer_sensor.dart';
export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart'; export 'multiplier/multiplier.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'rocket.dart'; export 'rocket.dart';
export 'score_text.dart'; export 'score_component.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'signpost.dart'; export 'signpost/signpost.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export 'spaceship_ramp.dart'; export 'spaceship_ramp.dart';

@ -23,59 +23,70 @@ class DinoWalls extends Component {
/// {@template dino_top_wall} /// {@template dino_top_wall}
/// Wall segment located above [ChromeDino]. /// Wall segment located above [ChromeDino].
/// {@endtemplate} /// {@endtemplate}
class _DinoTopWall extends BodyComponent with InitialPosition, ZIndex { class _DinoTopWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall} ///{@macro dino_top_wall}
_DinoTopWall() _DinoTopWall()
: super( : super(
children: [_DinoTopWallSpriteComponent()], children: [
_DinoTopWallSpriteComponent(),
_DinoTopWallTunnelSpriteComponent(),
],
renderBody: false, renderBody: false,
) { );
zIndex = ZIndexes.dinoTopWall;
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topStraightShape = EdgeShape() final topEdgeShape = EdgeShape()
..set( ..set(
Vector2(28.65, -34.3), Vector2(29.25, -35.27),
Vector2(29.5, -34.3), Vector2(28.4, -34.77),
); );
final topCurveShape = BezierCurveShape( final topCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
topStraightShape.vertex1, topEdgeShape.vertex2,
Vector2(18.8, -26.2), Vector2(21.35, -28.72),
Vector2(26.6, -20.2), Vector2(23.45, -24.62),
], ],
); );
final middleCurveShape = BezierCurveShape( final tunnelTopEdgeShape = EdgeShape()
controlPoints: [ ..set(
topCurveShape.vertices.last, topCurveShape.vertices.last,
Vector2(27.8, -19.3), Vector2(30.35, -27.32),
Vector2(26.8, -18.7), );
],
);
final bottomCurveShape = BezierCurveShape( final tunnelBottomEdgeShape = EdgeShape()
controlPoints: [ ..set(
middleCurveShape.vertices.last, Vector2(30.75, -23.17),
Vector2(23, -14.2), Vector2(25.45, -21.22),
Vector2(27, -14.2), );
],
);
final bottomStraightShape = EdgeShape() final middleEdgeShape = EdgeShape()
..set( ..set(
bottomCurveShape.vertices.last, tunnelBottomEdgeShape.vertex2,
Vector2(31, -13.7), Vector2(27.45, -19.32),
);
final bottomEdgeShape = EdgeShape()
..set(
middleEdgeShape.vertex2,
Vector2(24.65, -15.02),
);
final undersideEdgeShape = EdgeShape()
..set(
bottomEdgeShape.vertex2,
Vector2(31.75, -13.77),
); );
return [ return [
FixtureDef(topStraightShape), FixtureDef(topEdgeShape),
FixtureDef(topCurveShape), FixtureDef(topCurveShape),
FixtureDef(middleCurveShape), FixtureDef(tunnelTopEdgeShape),
FixtureDef(bottomCurveShape), FixtureDef(tunnelBottomEdgeShape),
FixtureDef(bottomStraightShape), FixtureDef(middleEdgeShape),
FixtureDef(bottomEdgeShape),
FixtureDef(undersideEdgeShape),
]; ];
} }
@ -93,7 +104,15 @@ class _DinoTopWall extends BodyComponent with InitialPosition, ZIndex {
} }
} }
class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { class _DinoTopWallSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_DinoTopWallSpriteComponent()
: super(
position: Vector2(22.75, -38.07),
) {
zIndex = ZIndexes.dinoTopWall;
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
@ -104,7 +123,26 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef {
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
position = Vector2(22.8, -38.1); }
}
class _DinoTopWallTunnelSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_DinoTopWallTunnelSpriteComponent()
: super(position: Vector2(23.31, -26.01)) {
zIndex = ZIndexes.dinoTopWallTunnel;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.dino.topWallTunnel.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
} }
} }
@ -122,7 +160,7 @@ class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex {
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topStraightShape = EdgeShape() final topEdgeShape = EdgeShape()
..set( ..set(
Vector2(32.4, -8.8), Vector2(32.4, -8.8),
Vector2(25, -7.7), Vector2(25, -7.7),
@ -130,29 +168,29 @@ class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex {
final topLeftCurveShape = BezierCurveShape( final topLeftCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
topStraightShape.vertex2, topEdgeShape.vertex2,
Vector2(21.8, -7), Vector2(21.8, -7),
Vector2(29.8, 13.8), Vector2(29.8, 13.8),
], ],
); );
final bottomLeftStraightShape = EdgeShape() final bottomLeftEdgeShape = EdgeShape()
..set( ..set(
topLeftCurveShape.vertices.last, topLeftCurveShape.vertices.last,
Vector2(31.9, 44.1), Vector2(31.9, 44.1),
); );
final bottomStraightShape = EdgeShape() final bottomEdgeShape = EdgeShape()
..set( ..set(
bottomLeftStraightShape.vertex2, bottomLeftEdgeShape.vertex2,
Vector2(37.8, 44.1), Vector2(37.8, 44.1),
); );
return [ return [
FixtureDef(topStraightShape), FixtureDef(topEdgeShape),
FixtureDef(topLeftCurveShape), FixtureDef(topLeftCurveShape),
FixtureDef(bottomLeftStraightShape), FixtureDef(bottomLeftEdgeShape),
FixtureDef(bottomStraightShape), FixtureDef(bottomEdgeShape),
]; ];
} }

@ -0,0 +1,15 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class FlapperSpinningBehavior extends ContactBehavior<FlapperEntrance> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.parent?.firstChild<SpriteAnimationComponent>()?.playing = true;
}
}

@ -0,0 +1,215 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/flapper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template flapper}
/// Flap to let a [Ball] out of the [LaunchRamp] and to prevent [Ball]s from
/// going back in.
/// {@endtemplate}
class Flapper extends Component {
/// {@macro flapper}
Flapper()
: super(
children: [
FlapperEntrance(
children: [
FlapperSpinningBehavior(),
],
)..initialPosition = Vector2(4, -69.3),
_FlapperStructure(),
_FlapperExit()..initialPosition = Vector2(-0.6, -33.8),
_BackSupportSpriteComponent(),
_FrontSupportSpriteComponent(),
FlapSpriteAnimationComponent(),
],
);
/// Creates a [Flapper] without any children.
///
/// This can be used for testing [Flapper]'s behaviors in isolation.
@visibleForTesting
Flapper.test();
}
/// {@template flapper_entrance}
/// Sensor used in [FlapperSpinningBehavior] to animate
/// [FlapSpriteAnimationComponent].
/// {@endtemplate}
class FlapperEntrance extends BodyComponent with InitialPosition, Layered {
/// {@macro flapper_entrance}
FlapperEntrance({
Iterable<Component>? children,
}) : super(
children: children,
renderBody: false,
) {
layer = Layer.launcher;
}
@override
Body createBody() {
final shape = EdgeShape()
..set(
Vector2.zero(),
Vector2(0, 3.2),
);
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(position: initialPosition);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _FlapperStructure extends BodyComponent with Layered {
_FlapperStructure() : super(renderBody: false) {
layer = Layer.board;
}
List<FixtureDef> _createFixtureDefs() {
final leftEdgeShape = EdgeShape()
..set(
Vector2(1.9, -69.3),
Vector2(1.9, -66),
);
final bottomEdgeShape = EdgeShape()
..set(
leftEdgeShape.vertex2,
Vector2(3.9, -66),
);
return [
FixtureDef(leftEdgeShape),
FixtureDef(bottomEdgeShape),
];
}
@override
Body createBody() {
final body = world.createBody(BodyDef());
_createFixtureDefs().forEach(body.createFixture);
return body;
}
}
class _FlapperExit extends LayerSensor {
_FlapperExit()
: super(
insideLayer: Layer.launcher,
outsideLayer: Layer.board,
orientation: LayerEntranceOrientation.down,
insideZIndex: ZIndexes.ballOnLaunchRamp,
outsideZIndex: ZIndexes.ballOnBoard,
) {
layer = Layer.launcher;
}
@override
Shape get shape => PolygonShape()
..setAsBox(
1.7,
0.1,
initialPosition,
1.5708,
);
}
/// {@template flap_sprite_animation_component}
/// Flap suspended between supports that animates to let the [Ball] exit the
/// [LaunchRamp].
/// {@endtemplate}
@visibleForTesting
class FlapSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef, ZIndex {
/// {@macro flap_sprite_animation_component}
FlapSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(2.8, -70.7),
playing: false,
) {
zIndex = ZIndexes.flapper;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.flapper.flap.keyName,
);
const amountPerRow = 14;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
loop: false,
),
)..onComplete = () {
animation?.reset();
playing = false;
};
}
}
class _BackSupportSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_BackSupportSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(2.95, -70.6),
) {
zIndex = ZIndexes.flapperBack;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.flapper.backSupport.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _FrontSupportSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_FrontSupportSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(2.9, -67.6),
) {
zIndex = ZIndexes.flapperFront;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.flapper.frontSupport.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

@ -5,7 +5,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
/// ///
/// Note: If the [initialPosition] is set after the [BodyComponent] has been /// Note: If the [initialPosition] is set after the [BodyComponent] has been
/// loaded it will have no effect; defaulting to [Vector2.zero]. /// loaded it will have no effect; defaulting to [Vector2.zero].
mixin InitialPosition<T extends Forge2DGame> on BodyComponent<T> { mixin InitialPosition on BodyComponent {
final Vector2 _initialPosition = Vector2.zero(); final Vector2 _initialPosition = Vector2.zero();
set initialPosition(Vector2 value) { set initialPosition(Vector2 value) {

@ -36,7 +36,7 @@ class Kicker extends BodyComponent with InitialPosition {
}) : _side = side, }) : _side = side,
super( super(
children: [ children: [
BumpingBehavior(strength: 20)..applyTo(['bouncy_edge']), BumpingBehavior(strength: 25)..applyTo(['bouncy_edge']),
KickerBallContactBehavior()..applyTo(['bouncy_edge']), KickerBallContactBehavior()..applyTo(['bouncy_edge']),
KickerBlinkingBehavior(), KickerBlinkingBehavior(),
_KickerSpriteGroupComponent( _KickerSpriteGroupComponent(

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

Loading…
Cancel
Save