Merge branch 'main' into release

release
Tom Arra 4 years ago
commit 482c8e2f95

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@ -0,0 +1,35 @@
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';
import 'package:pinball_theme/pinball_theme.dart';
/// Spawns a new [Ball] into the game when all balls are lost and still
/// [GameStatus.playing].
class BallSpawningBehavior extends Component
with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
@override
bool listenWhen(GameState? previousState, GameState newState) {
if (!newState.status.isPlaying) return false;
final startedGame = previousState?.status.isWaiting ?? true;
final lostRound =
(previousState?.rounds ?? newState.rounds + 1) > newState.rounds;
return startedGame || lostRound;
}
@override
void onNewState(GameState state) {
final plunger = gameRef.descendants().whereType<Plunger>().single;
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
final characterTheme = readProvider<CharacterTheme>();
final ball = ControlledBall.launch(characterTheme: characterTheme)
..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
canvas.add(ball);
}
}

@ -1,2 +1,4 @@
export 'bumper_noisy_behavior.dart'; export 'ball_spawning_behavior.dart';
export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';

@ -1,15 +1,13 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/pinball_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> { class BumperNoiseBehavior extends ContactBehavior {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);
gameRef.player.play(PinballAudio.bumper); readProvider<PinballPlayer>().play(PinballAudio.bumper);
} }
} }

@ -0,0 +1,83 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template focus_data}
/// Defines a [Camera] focus point.
/// {@endtemplate}
class FocusData {
/// {@template focus_data}
FocusData({
required this.zoom,
required this.position,
});
/// The amount of zoom.
final double zoom;
/// The position of the camera.
final Vector2 position;
}
/// Changes the game focus when the [GameBloc] status changes.
class CameraFocusingBehavior extends Component
with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
late final Map<String, FocusData> _foci;
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.status != newState.status;
}
@override
void onNewState(GameState state) {
switch (state.status) {
case GameStatus.waiting:
break;
case GameStatus.playing:
_zoom(_foci['game']!);
break;
case GameStatus.gameOver:
_zoom(_foci['backbox']!);
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
_foci = {
'game': FocusData(
zoom: gameRef.size.y / 16,
position: Vector2(0, -7.8),
),
'waiting': FocusData(
zoom: gameRef.size.y / 18,
position: Vector2(0, -112),
),
'backbox': FocusData(
zoom: gameRef.size.y / 10,
position: Vector2(0, -111),
),
};
_snap(_foci['waiting']!);
}
void _snap(FocusData data) {
gameRef.camera
..speed = 100
..followVector2(data.position)
..zoom = data.zoom;
}
void _zoom(FocusData data) {
final zoom = CameraZoom(value: data.zoom);
zoom.completed.then((_) {
gameRef.camera.moveTo(data.position);
});
add(zoom);
}
}

@ -2,6 +2,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/effects.dart'; import 'package:flame/effects.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.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';
@ -12,7 +13,8 @@ import 'package:pinball_flame/pinball_flame.dart';
/// ///
/// The behavior removes itself after the duration. /// The behavior removes itself after the duration.
/// {@endtemplate} /// {@endtemplate}
class ScoringBehavior extends Component with HasGameRef<PinballGame> { class ScoringBehavior extends Component
with HasGameRef, FlameBlocReader<GameBloc, GameState> {
/// {@macto scoring_behavior} /// {@macto scoring_behavior}
ScoringBehavior({ ScoringBehavior({
required Points points, required Points points,
@ -39,7 +41,8 @@ class ScoringBehavior extends Component with HasGameRef<PinballGame> {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
gameRef.read<GameBloc>().add(Scored(points: _points.value)); await super.onLoad();
bloc.add(Scored(points: _points.value));
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single; final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
await canvas.add( await canvas.add(
ScoreComponent( ScoreComponent(
@ -54,8 +57,7 @@ class ScoringBehavior extends Component with HasGameRef<PinballGame> {
/// {@template scoring_contact_behavior} /// {@template scoring_contact_behavior}
/// Adds points to the score when the [Ball] contacts the [parent]. /// Adds points to the score when the [Ball] contacts the [parent].
/// {@endtemplate} /// {@endtemplate}
class ScoringContactBehavior extends ContactBehavior class ScoringContactBehavior extends ContactBehavior {
with HasGameRef<PinballGame> {
/// {@macro scoring_contact_behavior} /// {@macro scoring_contact_behavior}
ScoringContactBehavior({ ScoringContactBehavior({
required Points points, required Points points,

@ -27,6 +27,7 @@ enum GameStatus {
} }
extension GameStatusX on GameStatus { extension GameStatusX on GameStatus {
bool get isWaiting => this == GameStatus.waiting;
bool get isPlaying => this == GameStatus.playing; bool get isPlaying => this == GameStatus.playing;
bool get isGameOver => this == GameStatus.gameOver; bool get isGameOver => this == GameStatus.gameOver;
} }

@ -35,21 +35,21 @@ class AndroidAcres extends Component {
AndroidBumper.a( AndroidBumper.a(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-25, 1.3), )..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b( AndroidBumper.b(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-32.8, -9.2), )..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow( AndroidBumper.cow(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-20.5, -13.8), )..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(), AndroidSpaceshipBonusBehavior(),
], ],
); );

@ -1,11 +1,12 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.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';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus. /// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus.
class AndroidSpaceshipBonusBehavior extends Component class AndroidSpaceshipBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<AndroidAcres> { with ParentIsA<AndroidAcres>, FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { void onMount() {
super.onMount(); super.onMount();
@ -18,9 +19,7 @@ class AndroidSpaceshipBonusBehavior extends Component
final listenWhen = state == AndroidSpaceshipState.withBonus; final listenWhen = state == AndroidSpaceshipState.withBonus;
if (!listenWhen) return; if (!listenWhen) return;
gameRef bloc.add(const BonusActivated(GameBonus.androidSpaceship));
.read<GameBloc>()
.add(const BonusActivated(GameBonus.androidSpaceship));
androidSpaceship.bloc.onBonusAwarded(); androidSpaceship.bloc.onBonusAwarded();
}); });
} }

@ -3,15 +3,13 @@ import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
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';
/// {@template ramp_bonus_behavior} /// {@template ramp_bonus_behavior}
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp]. /// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class RampBonusBehavior extends Component class RampBonusBehavior extends Component with ParentIsA<SpaceshipRamp> {
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
/// {@macro ramp_bonus_behavior} /// {@macro ramp_bonus_behavior}
RampBonusBehavior({ RampBonusBehavior({
required Points points, required Points points,

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
@ -11,7 +12,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. /// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class RampShotBehavior extends Component class RampShotBehavior extends Component
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> { with ParentIsA<SpaceshipRamp>, FlameBlocReader<GameBloc, GameState> {
/// {@macro ramp_shot_behavior} /// {@macro ramp_shot_behavior}
RampShotBehavior({ RampShotBehavior({
required Points points, required Points points,
@ -43,7 +44,7 @@ class RampShotBehavior extends Component
final achievedOneMillionPoints = state.hits % 10 == 0; final achievedOneMillionPoints = state.hits % 10 == 0;
if (!achievedOneMillionPoints) { if (!achievedOneMillionPoints) {
gameRef.read<GameBloc>().add(const MultiplierIncreased()); bloc.add(const MultiplierIncreased());
parent.add( parent.add(
ScoringBehavior( ScoringBehavior(

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart'; import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart'; import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball/game/pinball_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';
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
@ -13,7 +12,7 @@ import 'package:pinball_theme/pinball_theme.dart' hide Assets;
/// {@template backbox} /// {@template backbox}
/// The [Backbox] of the pinball machine. /// The [Backbox] of the pinball machine.
/// {@endtemplate} /// {@endtemplate}
class Backbox extends PositionComponent with HasGameRef<PinballGame>, ZIndex { class Backbox extends PositionComponent with ZIndex {
/// {@macro backbox} /// {@macro backbox}
Backbox({ Backbox({
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
@ -35,8 +34,11 @@ class Backbox extends PositionComponent with HasGameRef<PinballGame>, ZIndex {
anchor = Anchor.bottomCenter; anchor = Anchor.bottomCenter;
zIndex = ZIndexes.backbox; zIndex = ZIndexes.backbox;
_bloc.add(LeaderboardRequested());
await add(_BackboxSpriteComponent()); await add(_BackboxSpriteComponent());
await add(_display = Component()); await add(_display = Component());
_build(_bloc.state);
_subscription = _bloc.stream.listen((state) { _subscription = _bloc.stream.listen((state) {
_display.children.removeWhere((_) => true); _display.children.removeWhere((_) => true);
@ -53,6 +55,8 @@ class Backbox extends PositionComponent with HasGameRef<PinballGame>, ZIndex {
void _build(BackboxState state) { void _build(BackboxState state) {
if (state is LoadingState) { if (state is LoadingState) {
_display.add(LoadingDisplay()); _display.add(LoadingDisplay());
} else if (state is LeaderboardSuccessState) {
_display.add(LeaderboardDisplay(entries: state.entries));
} else if (state is InitialsFormState) { } else if (state is InitialsFormState) {
_display.add( _display.add(
InitialsInputDisplay( InitialsInputDisplay(

@ -18,6 +18,7 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
super(LoadingState()) { super(LoadingState()) {
on<PlayerInitialsRequested>(_onPlayerInitialsRequested); on<PlayerInitialsRequested>(_onPlayerInitialsRequested);
on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted); on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted);
on<LeaderboardRequested>(_onLeaderboardRequested);
} }
final LeaderboardRepository _leaderboardRepository; final LeaderboardRepository _leaderboardRepository;
@ -53,4 +54,20 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
emit(InitialsFailureState()); emit(InitialsFailureState());
} }
} }
Future<void> _onLeaderboardRequested(
LeaderboardRequested event,
Emitter<BackboxState> emit,
) async {
try {
emit(LoadingState());
final entries = await _leaderboardRepository.fetchTop10Leaderboard();
emit(LeaderboardSuccessState(entries: entries));
} catch (error, stackTrace) {
addError(error, stackTrace);
emit(LeaderboardFailureState());
}
}
} }

@ -51,3 +51,9 @@ class PlayerInitialsSubmitted extends BackboxEvent {
@override @override
List<Object?> get props => [score, initials, character]; List<Object?> get props => [score, initials, character];
} }
/// Event that triggers the fetching of the leaderboard
class LeaderboardRequested extends BackboxEvent {
@override
List<Object?> get props => [];
}

@ -14,10 +14,18 @@ class LoadingState extends BackboxState {
List<Object?> get props => []; List<Object?> get props => [];
} }
/// {@template leaderboard_success_state}
/// State when the leaderboard was successfully loaded. /// State when the leaderboard was successfully loaded.
/// {@endtemplate}
class LeaderboardSuccessState extends BackboxState { class LeaderboardSuccessState extends BackboxState {
/// {@macro leaderboard_success_state}
const LeaderboardSuccessState({required this.entries});
/// Current entries
final List<LeaderboardEntryData> entries;
@override @override
List<Object?> get props => []; List<Object?> get props => [entries];
} }
/// State when the leaderboard failed to load. /// State when the leaderboard failed to load.

@ -1,4 +1,5 @@
export 'initials_input_display.dart'; export 'initials_input_display.dart';
export 'initials_submission_failure_display.dart'; export 'initials_submission_failure_display.dart';
export 'initials_submission_success_display.dart'; export 'initials_submission_success_display.dart';
export 'leaderboard_display.dart';
export 'loading_display.dart'; export 'loading_display.dart';

@ -4,7 +4,7 @@ import 'dart:math';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.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';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -59,7 +59,7 @@ class InitialsInputDisplay extends Component with HasGameRef {
await add( await add(
InitialsLetterPrompt( InitialsLetterPrompt(
position: Vector2( position: Vector2(
11.4 + (2.3 * i), 10.8 + (2.5 * i),
-20, -20,
), ),
hasFocus: i == 0, hasFocus: i == 0,
@ -103,8 +103,7 @@ class InitialsInputDisplay extends Component with HasGameRef {
} }
} }
class _ScoreLabelTextComponent extends TextComponent class _ScoreLabelTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_ScoreLabelTextComponent() _ScoreLabelTextComponent()
: super( : super(
anchor: Anchor.centerLeft, anchor: Anchor.centerLeft,
@ -119,7 +118,7 @@ class _ScoreLabelTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.score; text = readProvider<AppLocalizations>().score;
} }
} }
@ -133,12 +132,11 @@ class _ScoreTextComponent extends TextComponent {
); );
} }
class _NameLabelTextComponent extends TextComponent class _NameLabelTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_NameLabelTextComponent() _NameLabelTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(11.4, -24), position: Vector2(10.8, -24),
textRenderer: _bodyTextPaint.copyWith( textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith( (style) => style.copyWith(
color: PinballColors.red, color: PinballColors.red,
@ -149,7 +147,7 @@ class _NameLabelTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.name; text = readProvider<AppLocalizations>().name;
} }
} }
@ -158,7 +156,7 @@ class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
: _characterIconPath = characterIconPath, : _characterIconPath = characterIconPath,
super( super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(8.4, -20), position: Vector2(7.6, -20),
); );
final String _characterIconPath; final String _characterIconPath;
@ -241,8 +239,9 @@ class InitialsLetterPrompt extends PositionComponent {
bool _cycle(bool up) { bool _cycle(bool up) {
if (_hasFocus) { if (_hasFocus) {
final newCharCode = var newCharCode = _charIndex + (up ? -1 : 1);
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength); if (newCharCode < 0) newCharCode = _alphabetLength;
if (newCharCode > _alphabetLength) newCharCode = 0;
_input.text = String.fromCharCode(_alphabetCode + newCharCode); _input.text = String.fromCharCode(_alphabetCode + newCharCode);
_charIndex = newCharCode; _charIndex = newCharCode;
@ -299,8 +298,7 @@ class _InstructionsComponent extends PositionComponent with HasGameRef {
); );
} }
class _EnterInitialsTextComponent extends TextComponent class _EnterInitialsTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_EnterInitialsTextComponent() _EnterInitialsTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -311,11 +309,11 @@ class _EnterInitialsTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.enterInitials; text = readProvider<AppLocalizations>().enterInitials;
} }
} }
class _ArrowsTextComponent extends TextComponent with HasGameRef<PinballGame> { class _ArrowsTextComponent extends TextComponent {
_ArrowsTextComponent() _ArrowsTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -330,12 +328,11 @@ class _ArrowsTextComponent extends TextComponent with HasGameRef<PinballGame> {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.arrows; text = readProvider<AppLocalizations>().arrows;
} }
} }
class _AndPressTextComponent extends TextComponent class _AndPressTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_AndPressTextComponent() _AndPressTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -346,12 +343,11 @@ class _AndPressTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.andPress; text = readProvider<AppLocalizations>().andPress;
} }
} }
class _EnterReturnTextComponent extends TextComponent class _EnterReturnTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_EnterReturnTextComponent() _EnterReturnTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -366,12 +362,11 @@ class _EnterReturnTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.enterReturn; text = readProvider<AppLocalizations>().enterReturn;
} }
} }
class _ToSubmitTextComponent extends TextComponent class _ToSubmitTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_ToSubmitTextComponent() _ToSubmitTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -382,6 +377,6 @@ class _ToSubmitTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.toSubmit; text = readProvider<AppLocalizations>().toSubmit;
} }
} }

@ -15,8 +15,7 @@ final _bodyTextPaint = TextPaint(
/// {@template initials_submission_failure_display} /// {@template initials_submission_failure_display}
/// [Backbox] display for when a failure occurs during initials submission. /// [Backbox] display for when a failure occurs during initials submission.
/// {@endtemplate} /// {@endtemplate}
class InitialsSubmissionFailureDisplay extends TextComponent class InitialsSubmissionFailureDisplay extends TextComponent {
with HasGameRef<PinballGame> {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();

@ -15,8 +15,7 @@ final _bodyTextPaint = TextPaint(
/// {@template initials_submission_success_display} /// {@template initials_submission_success_display}
/// [Backbox] display for initials successfully submitted. /// [Backbox] display for initials successfully submitted.
/// {@endtemplate} /// {@endtemplate}
class InitialsSubmissionSuccessDisplay extends TextComponent class InitialsSubmissionSuccessDisplay extends TextComponent {
with HasGameRef<PinballGame> {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();

@ -0,0 +1,120 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/leaderboard/models/leader_board_entry.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
final _titleTextPaint = TextPaint(
style: const TextStyle(
fontSize: 2,
color: PinballColors.red,
fontFamily: PinballFonts.pixeloidSans,
),
);
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.8,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template leaderboard_display}
/// Component that builds the leaderboard list of the Backbox.
/// {@endtemplate}
class LeaderboardDisplay extends PositionComponent with HasGameRef {
/// {@macro leaderboard_display}
LeaderboardDisplay({required List<LeaderboardEntryData> entries})
: _entries = entries;
final List<LeaderboardEntryData> _entries;
double _calcY(int i) => (i * 3.2) + 3.2;
static const _columns = [-15.0, 0.0, 15.0];
String _rank(int number) {
switch (number) {
case 1:
return '${number}st';
case 2:
return '${number}nd';
case 3:
return '${number}rd';
default:
return '${number}th';
}
}
@override
Future<void> onLoad() async {
position = Vector2(0, -30);
final l10n = readProvider<AppLocalizations>();
final ranking = _entries.take(5).toList();
await add(
PositionComponent(
position: Vector2(0, 4),
children: [
PositionComponent(
children: [
TextComponent(
text: l10n.rank,
textRenderer: _titleTextPaint,
position: Vector2(_columns[0], 0),
anchor: Anchor.center,
),
TextComponent(
text: l10n.score,
textRenderer: _titleTextPaint,
position: Vector2(_columns[1], 0),
anchor: Anchor.center,
),
TextComponent(
text: l10n.name,
textRenderer: _titleTextPaint,
position: Vector2(_columns[2], 0),
anchor: Anchor.center,
),
],
),
for (var i = 0; i < ranking.length; i++)
PositionComponent(
children: [
TextComponent(
text: _rank(i + 1),
textRenderer: _bodyTextPaint,
position: Vector2(_columns[0], _calcY(i)),
anchor: Anchor.center,
),
TextComponent(
text: ranking[i].score.formatScore(),
textRenderer: _bodyTextPaint,
position: Vector2(_columns[1], _calcY(i)),
anchor: Anchor.center,
),
SpriteComponent.fromImage(
gameRef.images.fromCache(
ranking[i].character.toTheme.leaderboardIcon.keyName,
),
anchor: Anchor.center,
size: Vector2(1.8, 1.8),
position: Vector2(_columns[2] - 2.5, _calcY(i) + .25),
),
TextComponent(
text: ranking[i].playerInitials,
textRenderer: _bodyTextPaint,
position: Vector2(_columns[2] + 1, _calcY(i)),
anchor: Anchor.center,
),
],
),
],
),
);
}
}

@ -1,7 +1,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.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_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
final _bodyTextPaint = TextPaint( final _bodyTextPaint = TextPaint(
@ -15,7 +16,7 @@ final _bodyTextPaint = TextPaint(
/// {@template loading_display} /// {@template loading_display}
/// Display used to show the loading animation. /// Display used to show the loading animation.
/// {@endtemplate} /// {@endtemplate}
class LoadingDisplay extends TextComponent with HasGameRef<PinballGame> { class LoadingDisplay extends TextComponent {
/// {@template loading_display} /// {@template loading_display}
LoadingDisplay(); LoadingDisplay();
@ -27,7 +28,7 @@ class LoadingDisplay extends TextComponent with HasGameRef<PinballGame> {
position = Vector2(0, -10); position = Vector2(0, -10);
anchor = Anchor.center; anchor = Anchor.center;
text = _label = gameRef.l10n.loading; text = _label = readProvider<AppLocalizations>().loading;
textRenderer = _bodyTextPaint; textRenderer = _bodyTextPaint;
await add( await add(

@ -39,14 +39,14 @@ class _BottomGroupSide extends Component {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
final direction = _side.direction; final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? 0 : -6.66; final centerXAdjustment = _side.isLeft ? -0.45 : -6.8;
final flipper = ControlledFlipper( final flipper = ControlledFlipper(
side: _side, side: _side,
)..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); )..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side) final baseboard = Baseboard(side: _side)
..initialPosition = Vector2( ..initialPosition = Vector2(
(25.58 * direction) + centerXAdjustment, (25.38 * direction) + centerXAdjustment,
28.71, 28.71,
); );
final kicker = Kicker( final kicker = Kicker(
@ -56,7 +56,7 @@ class _BottomGroupSide extends Component {
..applyTo(['bouncy_edge']), ..applyTo(['bouncy_edge']),
], ],
)..initialPosition = Vector2( )..initialPosition = Vector2(
(22.64 * direction) + centerXAdjustment, (22.44 * direction) + centerXAdjustment,
25.1, 25.1,
); );

@ -1,93 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds helpers methods to Flame's [Camera].
extension CameraX on Camera {
/// Instantly apply the point of focus to the [Camera].
void snapToFocus(FocusData data) {
followVector2(data.position);
zoom = data.zoom;
}
/// Returns a [CameraZoom] that can be added to a [FlameGame].
CameraZoom focusToCameraZoom(FocusData data) {
final zoom = CameraZoom(value: data.zoom);
zoom.completed.then((_) {
moveTo(data.position);
});
return zoom;
}
}
/// {@template focus_data}
/// Model class that defines a focus point of the camera.
/// {@endtemplate}
class FocusData {
/// {@template focus_data}
FocusData({
required this.zoom,
required this.position,
});
/// The amount of zoom.
final double zoom;
/// The position of the camera.
final Vector2 position;
}
/// {@template camera_controller}
/// A [Component] that controls its game camera focus.
/// {@endtemplate}
class CameraController extends ComponentController<FlameGame> {
/// {@macro camera_controller}
CameraController(FlameGame component) : super(component) {
final gameZoom = component.size.y / 16;
final waitingBackboxZoom = component.size.y / 18;
final gameOverBackboxZoom = component.size.y / 10;
gameFocus = FocusData(
zoom: gameZoom,
position: Vector2(0, -7.8),
);
waitingBackboxFocus = FocusData(
zoom: waitingBackboxZoom,
position: Vector2(0, -112),
);
gameOverBackboxFocus = FocusData(
zoom: gameOverBackboxZoom,
position: Vector2(0, -111),
);
// Game starts with the camera focused on the [Backbox].
component.camera
..speed = 100
..snapToFocus(waitingBackboxFocus);
}
/// Holds the data for the game focus point.
late final FocusData gameFocus;
/// Holds the data for the waiting backbox focus point.
late final FocusData waitingBackboxFocus;
/// Holds the data for the game over backbox focus point.
late final FocusData gameOverBackboxFocus;
/// Move the camera focus to the game board.
void focusOnGame() {
component.add(component.camera.focusToCameraZoom(gameFocus));
}
/// Move the camera focus to the waiting backbox.
void focusOnWaitingBackbox() {
component.add(component.camera.focusToCameraZoom(waitingBackboxFocus));
}
/// Move the camera focus to the game over backbox.
void focusOnGameOverBackbox() {
component.add(component.camera.focusToCameraZoom(gameOverBackboxFocus));
}
}

@ -1,12 +1,11 @@
export 'android_acres/android_acres.dart'; export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart'; export 'backbox/backbox.dart';
export 'bottom_group.dart'; export 'bottom_group.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/dino_desert.dart'; export 'dino_desert/dino_desert.dart';
export 'drain.dart'; export 'drain/drain.dart';
export 'flutter_forest/flutter_forest.dart'; export 'flutter_forest/flutter_forest.dart';
export 'game_bloc_status_listener.dart'; export 'game_bloc_status_listener.dart';
export 'google_word/google_word.dart'; export 'google_word/google_word.dart';

@ -1,6 +1,7 @@
// 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:flame_bloc/flame_bloc.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';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -22,9 +23,7 @@ class ControlledBall extends Ball with Controls<BallController> {
zIndex = ZIndexes.ballOnLaunchRamp; zIndex = ZIndexes.ballOnLaunchRamp;
} }
/// {@template bonus_ball}
/// {@macro controlled_ball} /// {@macro controlled_ball}
/// {@endtemplate}
ControlledBall.bonus({ ControlledBall.bonus({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) { }) : super(assetPath: characterTheme.ball.keyName) {
@ -43,20 +42,14 @@ class ControlledBall extends Ball with Controls<BallController> {
/// Controller attached to a [Ball] that handles its game related logic. /// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate} /// {@endtemplate}
class BallController extends ComponentController<Ball> class BallController extends ComponentController<Ball>
with HasGameRef<PinballGame> { with FlameBlocReader<GameBloc, GameState> {
/// {@macro ball_controller} /// {@macro ball_controller}
BallController(Ball ball) : super(ball); BallController(Ball ball) : super(ball);
/// Event triggered when the ball is lost.
// TODO(alestiago): Refactor using behaviors.
void lost() {
component.shouldRemove = true;
}
/// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge /// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge
/// sequence runs, then boosts the ball out of the computer. /// sequence runs, then boosts the ball out of the computer.
Future<void> turboCharge() async { Future<void> turboCharge() async {
gameRef.read<GameBloc>().add(const SparkyTurboChargeActivated()); bloc.add(const SparkyTurboChargeActivated());
component.stop(); component.stop();
// TODO(alestiago): Refactor this hard coded duration once the following is // TODO(alestiago): Refactor this hard coded duration once the following is
@ -70,13 +63,4 @@ class BallController extends ComponentController<Ball>
BallTurboChargingBehavior(impulse: Vector2(40, 110)), BallTurboChargingBehavior(impulse: Vector2(40, 110)),
); );
} }
@override
void onRemove() {
super.onRemove();
final noBallsLeft = gameRef.descendants().whereType<Ball>().isEmpty;
if (noBallsLeft) {
gameRef.read<GameBloc>().add(const RoundLost());
}
}
} }

@ -21,7 +21,7 @@ class ControlledFlipper extends Flipper with Controls<FlipperController> {
/// A [ComponentController] that controls a [Flipper]s movement. /// A [ComponentController] that controls a [Flipper]s movement.
/// {@endtemplate} /// {@endtemplate}
class FlipperController extends ComponentController<Flipper> class FlipperController extends ComponentController<Flipper>
with KeyboardHandler, BlocComponent<GameBloc, GameState> { with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro flipper_controller} /// {@macro flipper_controller}
FlipperController(Flipper flipper) FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys, : _keys = flipper.side.flipperKeys,
@ -37,7 +37,7 @@ class FlipperController extends ComponentController<Flipper>
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (state?.status.isGameOver ?? false) return true; if (!bloc.state.status.isPlaying) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -2,6 +2,7 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.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';
@ -14,13 +15,36 @@ class ControlledPlunger extends Plunger with Controls<PlungerController> {
: super(compressionDistance: compressionDistance) { : super(compressionDistance: compressionDistance) {
controller = PlungerController(this); controller = PlungerController(this);
} }
@override
void release() {
super.release();
add(PlungerNoiseBehavior());
}
}
/// A behavior attached to the plunger when it launches the ball which plays the
/// related sound effects.
class PlungerNoiseBehavior extends Component {
@override
Future<void> onLoad() async {
await super.onLoad();
readProvider<PinballPlayer>().play(PinballAudio.launcher);
}
@override
void update(double dt) {
super.update(dt);
removeFromParent();
}
} }
/// {@template plunger_controller} /// {@template plunger_controller}
/// A [ComponentController] that controls a [Plunger]s movement. /// A [ComponentController] that controls a [Plunger]s movement.
/// {@endtemplate} /// {@endtemplate}
class PlungerController extends ComponentController<Plunger> class PlungerController extends ComponentController<Plunger>
with KeyboardHandler, BlocComponent<GameBloc, GameState> { with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro plunger_controller} /// {@macro plunger_controller}
PlungerController(Plunger plunger) : super(plunger); PlungerController(Plunger plunger) : super(plunger);
@ -38,7 +62,7 @@ class PlungerController extends ComponentController<Plunger>
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (state?.status.isGameOver ?? false) return true; if (bloc.state.status.isGameOver) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -1,11 +1,12 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.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';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.dinoChomp] when a [Ball] is chomped by the [ChromeDino]. /// Adds a [GameBonus.dinoChomp] when a [Ball] is chomped by the [ChromeDino].
class ChromeDinoBonusBehavior extends Component class ChromeDinoBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<DinoDesert> { with ParentIsA<DinoDesert>, FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { void onMount() {
super.onMount(); super.onMount();
@ -18,7 +19,7 @@ class ChromeDinoBonusBehavior extends Component
final listenWhen = state.status == ChromeDinoStatus.chomping; final listenWhen = state.status == ChromeDinoStatus.chomping;
if (!listenWhen) return; if (!listenWhen) return;
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.dinoChomp)); bloc.add(const BonusActivated(GameBonus.dinoChomp));
}); });
} }
} }

@ -20,7 +20,7 @@ class DinoDesert extends Component {
ScoringContactBehavior(points: Points.twoHundredThousand) ScoringContactBehavior(points: Points.twoHundredThousand)
..applyTo(['inside_mouth']), ..applyTo(['inside_mouth']),
], ],
)..initialPosition = Vector2(12.6, -6.9), )..initialPosition = Vector2(12.2, -6.9),
_BarrierBehindDino(), _BarrierBehindDino(),
DinoWalls(), DinoWalls(),
Slingshots(), Slingshots(),

@ -1,34 +0,0 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template drain}
/// Area located at the bottom of the board to detect when a [Ball] is lost.
/// {@endtemplate}
// TODO(allisonryan0002): move to components package when possible.
class Drain extends BodyComponent with ContactCallbacks {
/// {@macro drain}
Drain() : super(renderBody: false);
@override
Body createBody() {
final shape = EdgeShape()
..set(
BoardDimensions.bounds.bottomLeft.toVector2(),
BoardDimensions.bounds.bottomRight.toVector2(),
);
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef(userData: this);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
// TODO(allisonryan0002): move this to ball.dart when BallLost is removed.
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.lost();
}
}

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

@ -0,0 +1,25 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Handles removing a [Ball] from the game.
class DrainingBehavior extends ContactBehavior<Drain> with HasGameRef {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
other.removeFromParent();
final ballsLeft = gameRef.descendants().whereType<Ball>().length;
if (ballsLeft - 1 == 0) {
ancestors()
.whereType<FlameBlocProvider<GameBloc, GameState>>()
.first
.bloc
.add(const RoundLost());
}
}
}

@ -0,0 +1,36 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'package:pinball/game/components/drain/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template drain}
/// Area located at the bottom of the board.
///
/// Its [DrainingBehavior] handles removing a [Ball] from the game.
/// {@endtemplate}
class Drain extends BodyComponent with ContactCallbacks {
/// {@macro drain}
Drain()
: super(
renderBody: false,
children: [DrainingBehavior()],
);
/// Creates a [Drain] without any children.
///
/// This can be used for testing a [Drain]'s behaviors in isolation.
@visibleForTesting
Drain.test();
@override
Body createBody() {
final shape = EdgeShape()
..set(
BoardDimensions.bounds.bottomLeft.toVector2(),
BoardDimensions.bounds.bottomRight.toVector2(),
);
final fixtureDef = FixtureDef(shape, isSensor: true);
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}

@ -1,7 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.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';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// Bonus obtained at the [FlutterForest]. /// Bonus obtained at the [FlutterForest].
/// ///
@ -9,7 +11,10 @@ import 'package:pinball_flame/pinball_flame.dart';
/// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest] /// 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,
FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { void onMount() {
super.onMount(); super.onMount();
@ -35,12 +40,11 @@ class FlutterForestBonusBehavior extends Component
} }
if (signpost.bloc.isFullyProgressed()) { if (signpost.bloc.isFullyProgressed()) {
gameRef bloc.add(const BonusActivated(GameBonus.dashNest));
.read<GameBloc>()
.add(const BonusActivated(GameBonus.dashNest));
canvas.add( canvas.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme) ControlledBall.bonus(
..initialPosition = Vector2(29.5, -24.5), characterTheme: readProvider<CharacterTheme>(),
)..initialPosition = Vector2(29.2, -24.5),
); );
animatronic.playing = true; animatronic.playing = true;
signpost.bloc.onProgressed(); signpost.bloc.onProgressed();

@ -19,27 +19,27 @@ class FlutterForest extends Component with ZIndex {
Signpost( Signpost(
children: [ children: [
ScoringContactBehavior(points: Points.fiveThousand), ScoringContactBehavior(points: Points.fiveThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(8.35, -58.3), )..initialPosition = Vector2(7.95, -58.35),
DashNestBumper.main( DashNestBumper.main(
children: [ children: [
ScoringContactBehavior(points: Points.twoHundredThousand), ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(18.55, -59.35), )..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a( DashNestBumper.a(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(8.95, -51.95), )..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b( DashNestBumper.b(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(22.3, -46.75), )..initialPosition = Vector2(21.8, -46.75),
DashAnimatronic()..position = Vector2(20, -66), DashAnimatronic()..position = Vector2(20, -66),
FlutterForestBonusBehavior(), FlutterForestBonusBehavior(),
], ],

@ -2,10 +2,12 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// Listens to the [GameBloc] and updates the game accordingly. /// Listens to the [GameBloc] and updates the game accordingly.
class GameBlocStatusListener extends Component class GameBlocStatusListener extends Component
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> { with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.status != newState.status; return previousState?.status != newState.status;
@ -17,16 +19,15 @@ class GameBlocStatusListener extends Component
case GameStatus.waiting: case GameStatus.waiting:
break; break;
case GameStatus.playing: case GameStatus.playing:
gameRef.player.play(PinballAudio.backgroundMusic); readProvider<PinballPlayer>().play(PinballAudio.backgroundMusic);
gameRef.firstChild<CameraController>()?.focusOnGame();
gameRef.overlays.remove(PinballGame.playButtonOverlay); gameRef.overlays.remove(PinballGame.playButtonOverlay);
break; break;
case GameStatus.gameOver: case GameStatus.gameOver:
readProvider<PinballPlayer>().play(PinballAudio.gameOverVoiceOver);
gameRef.descendants().whereType<Backbox>().first.requestInitials( gameRef.descendants().whereType<Backbox>().first.requestInitials(
score: state.displayScore, score: state.displayScore,
character: gameRef.characterTheme, character: readProvider<CharacterTheme>(),
); );
gameRef.firstChild<CameraController>()!.focusOnGameOverBackbox();
break; break;
} }
} }

@ -1,4 +1,5 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -6,7 +7,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated. /// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated.
class GoogleWordBonusBehavior extends Component class GoogleWordBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<GoogleWord> { with ParentIsA<GoogleWord>, FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { void onMount() {
super.onMount(); super.onMount();
@ -21,10 +22,8 @@ class GoogleWordBonusBehavior extends Component
.every((letter) => letter.bloc.state == GoogleLetterState.lit); .every((letter) => letter.bloc.state == GoogleLetterState.lit);
if (achievedBonus) { if (achievedBonus) {
gameRef.player.play(PinballAudio.google); readProvider<PinballPlayer>().play(PinballAudio.google);
gameRef bloc.add(const BonusActivated(GameBonus.googleWord));
.read<GameBloc>()
.add(const BonusActivated(GameBonus.googleWord));
for (final letter in googleLetters) { for (final letter in googleLetters) {
letter.bloc.onReset(); letter.bloc.onReset();
} }

@ -14,8 +14,8 @@ class Launcher extends Component {
LaunchRamp(), LaunchRamp(),
Flapper(), Flapper(),
ControlledPlunger(compressionDistance: 9.2) ControlledPlunger(compressionDistance: 9.2)
..initialPosition = Vector2(41.2, 43.7), ..initialPosition = Vector2(41, 43.7),
RocketSpriteComponent()..position = Vector2(43, 62.3), RocketSpriteComponent()..position = Vector2(42.8, 62.3),
], ],
); );
} }

@ -6,10 +6,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Toggle each [Multiball] when there is a bonus ball. /// Toggle each [Multiball] when there is a bonus ball.
class MultiballsBehavior extends Component class MultiballsBehavior extends Component
with with ParentIsA<Multiballs>, FlameBlocListenable<GameBloc, GameState> {
HasGameRef<PinballGame>,
ParentIsA<Multiballs>,
BlocComponent<GameBloc, GameState> {
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
final hasChanged = previousState?.bonusHistory != newState.bonusHistory; final hasChanged = previousState?.bonusHistory != newState.bonusHistory;

@ -6,10 +6,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Toggle each [Multiplier] when GameState.multiplier changes. /// Toggle each [Multiplier] when GameState.multiplier changes.
class MultipliersBehavior extends Component class MultipliersBehavior extends Component
with with ParentIsA<Multipliers>, FlameBlocListenable<GameBloc, GameState> {
HasGameRef<PinballGame>,
ParentIsA<Multipliers>,
BlocComponent<GameBloc, GameState> {
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.multiplier != newState.multiplier; return previousState?.multiplier != newState.multiplier;

@ -14,23 +14,23 @@ class Multipliers extends Component with ZIndex {
: super( : super(
children: [ children: [
Multiplier.x2( Multiplier.x2(
position: Vector2(-19.5, -2), position: Vector2(-19.6, -2),
angle: -15 * math.pi / 180, angle: -15 * math.pi / 180,
), ),
Multiplier.x3( Multiplier.x3(
position: Vector2(13, -9.4), position: Vector2(12.8, -9.4),
angle: 15 * math.pi / 180, angle: 15 * math.pi / 180,
), ),
Multiplier.x4( Multiplier.x4(
position: Vector2(0, -21.2), position: Vector2(-0.3, -21.2),
angle: 0, angle: 3 * math.pi / 180,
), ),
Multiplier.x5( Multiplier.x5(
position: Vector2(-8.5, -28), position: Vector2(-8.9, -28),
angle: -3 * math.pi / 180, angle: -3 * math.pi / 180,
), ),
Multiplier.x6( Multiplier.x6(
position: Vector2(10, -30.7), position: Vector2(9.8, -30.7),
angle: 8 * math.pi / 180, angle: 8 * math.pi / 180,
), ),
MultipliersBehavior(), MultipliersBehavior(),

@ -18,23 +18,23 @@ class SparkyScorch extends Component {
SparkyBumper.a( SparkyBumper.a(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-22.9, -41.65), )..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b( SparkyBumper.b(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-21.25, -57.9), )..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c( SparkyBumper.c(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9), SparkyComputerSensor()..initialPosition = Vector2(-13.2, -49.9),
SparkyAnimatronic()..position = Vector2(-13.8, -58.2), SparkyAnimatronic()..position = Vector2(-14, -58.2),
SparkyComputer(), SparkyComputer(),
], ],
); );

@ -17,19 +17,21 @@ import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends PinballForge2DGame class PinballGame extends PinballForge2DGame
with with HasKeyboardHandlerComponents, MultiTouchTapDetector {
FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController>,
MultiTouchTapDetector {
PinballGame({ PinballGame({
required this.characterTheme, required CharacterTheme characterTheme,
required this.leaderboardRepository, required this.leaderboardRepository,
required this.l10n, required GameBloc gameBloc,
required this.player, required AppLocalizations l10n,
}) : super(gravity: Vector2(0, 30)) { required PinballPlayer player,
}) : _gameBloc = gameBloc,
_player = player,
_characterTheme = characterTheme,
_l10n = l10n,
super(
gravity: Vector2(0, 30),
) {
images.prefix = ''; images.prefix = '';
controller = _GameBallsController(this);
} }
/// Identifier of the play button overlay /// Identifier of the play button overlay
@ -38,25 +40,46 @@ class PinballGame extends PinballForge2DGame
@override @override
Color backgroundColor() => Colors.transparent; Color backgroundColor() => Colors.transparent;
final CharacterTheme characterTheme; final CharacterTheme _characterTheme;
final PinballPlayer player; final PinballPlayer _player;
final LeaderboardRepository leaderboardRepository; final LeaderboardRepository leaderboardRepository;
final AppLocalizations l10n; final AppLocalizations _l10n;
final GameBloc _gameBloc;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await add(CameraController(this)); await add(
FlameBlocProvider<GameBloc, GameState>.value(
final machine = [ value: _gameBloc,
children: [
MultiFlameProvider(
providers: [
FlameProvider<PinballPlayer>.value(_player),
FlameProvider<CharacterTheme>.value(_characterTheme),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<AppLocalizations>.value(_l10n),
],
children: [
GameBlocStatusListener(),
BallSpawningBehavior(),
CameraFocusingBehavior(),
CanvasComponent(
onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [
ZCanvasComponent(
children: [
BoardBackgroundSpriteComponent(), BoardBackgroundSpriteComponent(),
Boundaries(), Boundaries(),
Backbox(leaderboardRepository: leaderboardRepository), Backbox(leaderboardRepository: leaderboardRepository),
]; GoogleWord(position: Vector2(-4.45, 1.8)),
final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(), Multipliers(),
Multiballs(), Multiballs(),
SkillShot( SkillShot(
@ -64,29 +87,10 @@ class PinballGame extends PinballForge2DGame
ScoringContactBehavior(points: Points.oneMillion), ScoringContactBehavior(points: Points.oneMillion),
], ],
), ),
];
final characterAreas = [
AndroidAcres(), AndroidAcres(),
DinoDesert(), DinoDesert(),
FlutterForest(), FlutterForest(),
SparkyScorch(), SparkyScorch(),
];
await addAll(
[
GameBlocStatusListener(),
CanvasComponent(
onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [
ZCanvasComponent(
children: [
...machine,
...decals,
...characterAreas,
Drain(), Drain(),
BottomGroup(), BottomGroup(),
Launcher(), Launcher(),
@ -95,6 +99,9 @@ class PinballGame extends PinballForge2DGame
], ],
), ),
], ],
),
],
),
); );
await super.onLoad(); await super.onLoad();
@ -148,57 +155,20 @@ class PinballGame extends PinballForge2DGame
} }
} }
class _GameBallsController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState> {
_GameBallsController(PinballGame game) : super(game);
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
return noBallsLeft && newState.status.isPlaying;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
spawnBall();
}
@override
Future<void> onLoad() async {
await super.onLoad();
spawnBall();
}
void spawnBall() {
// TODO(alestiago): Refactor with behavioural pattern.
component.ready().whenComplete(() {
final plunger = parent!.descendants().whereType<Plunger>().single;
final ball = ControlledBall.launch(
characterTheme: component.characterTheme,
)..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
component.descendants().whereType<ZCanvasComponent>().single.add(ball);
});
}
}
class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({ DebugPinballGame({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballPlayer player, required PinballPlayer player,
required GameBloc gameBloc,
}) : super( }) : super(
characterTheme: characterTheme, characterTheme: characterTheme,
player: player, player: player,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
l10n: l10n, l10n: l10n,
) { gameBloc: gameBloc,
controller = _GameBallsController(this); );
}
Vector2? lineStart; Vector2? lineStart;
Vector2? lineEnd; Vector2? lineEnd;

@ -40,18 +40,25 @@ class PinballGamePage extends StatelessWidget {
final player = context.read<PinballPlayer>(); final player = context.read<PinballPlayer>();
final leaderboardRepository = context.read<LeaderboardRepository>(); final leaderboardRepository = context.read<LeaderboardRepository>();
return BlocProvider(
create: (_) => GameBloc(),
child: Builder(
builder: (context) {
final gameBloc = context.read<GameBloc>();
final game = isDebugMode final game = isDebugMode
? DebugPinballGame( ? DebugPinballGame(
characterTheme: characterTheme, characterTheme: characterTheme,
player: player, player: player,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
l10n: context.l10n, l10n: context.l10n,
gameBloc: gameBloc,
) )
: PinballGame( : PinballGame(
characterTheme: characterTheme, characterTheme: characterTheme,
player: player, player: player,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
l10n: context.l10n, l10n: context.l10n,
gameBloc: gameBloc,
); );
final loadables = [ final loadables = [
@ -61,13 +68,13 @@ class PinballGamePage extends StatelessWidget {
...SelectedCharacter.loadAssets(), ...SelectedCharacter.loadAssets(),
]; ];
return MultiBlocProvider( return BlocProvider(
providers: [ create: (_) => AssetsManagerCubit(loadables)..load(),
BlocProvider(create: (_) => GameBloc()),
BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()),
],
child: PinballGameView(game: game), child: PinballGameView(game: game),
); );
},
),
);
} }
} }
@ -107,14 +114,6 @@ class PinballGameLoadedView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isPlaying = context.select(
(StartGameBloc bloc) => bloc.state.status == StartGameStatus.play,
);
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
final clampedMargin = leftMargin > 0 ? leftMargin : 0.0;
return StartGameListener( return StartGameListener(
child: Stack( child: Stack(
children: [ children: [
@ -134,16 +133,36 @@ class PinballGameLoadedView extends StatelessWidget {
}, },
), ),
), ),
Positioned( const _PositionedGameHud(),
],
),
);
}
}
class _PositionedGameHud extends StatelessWidget {
const _PositionedGameHud({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isPlaying = context.select(
(StartGameBloc bloc) => bloc.state.status == StartGameStatus.play,
);
final isGameOver = context.select(
(GameBloc bloc) => bloc.state.status.isGameOver,
);
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
final clampedMargin = leftMargin > 0 ? leftMargin : 0.0;
return Positioned(
top: 0, top: 0,
left: clampedMargin, left: clampedMargin,
child: Visibility( child: Visibility(
visible: isPlaying, visible: isPlaying && !isGameOver,
child: const GameHud(), child: const GameHud(),
), ),
),
],
),
); );
} }
} }

@ -17,9 +17,10 @@ class ScoreView extends StatelessWidget {
context.select((GameBloc bloc) => bloc.state.status.isGameOver); context.select((GameBloc bloc) => bloc.state.status.isGameOver);
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.only(
horizontal: 16, left: 12,
vertical: 2, top: 2,
bottom: 2,
), ),
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: kThemeAnimationDuration, duration: kThemeAnimationDuration,
@ -50,7 +51,10 @@ class _ScoreDisplay extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return Column( return Row(
children: [
FittedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
@ -61,6 +65,9 @@ class _ScoreDisplay extends StatelessWidget {
const _ScoreText(), const _ScoreText(),
const RoundCountDisplay(), const RoundCountDisplay(),
], ],
),
),
],
); );
} }
} }
@ -72,11 +79,9 @@ class _ScoreText extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final score = context.select((GameBloc bloc) => bloc.state.displayScore); final score = context.select((GameBloc bloc) => bloc.state.displayScore);
return FittedBox( return Text(
child: Text(
score.formatScore(), score.formatScore(),
style: Theme.of(context).textTheme.headline2, style: Theme.of(context).textTheme.headline1,
),
); );
} }
} }

@ -14,6 +14,7 @@ class $AssetsImagesGen {
const $AssetsImagesBonusAnimationGen(); const $AssetsImagesBonusAnimationGen();
$AssetsImagesComponentsGen get components => $AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
} }
@ -53,6 +54,14 @@ class $AssetsImagesComponentsGen {
const AssetGenImage('assets/images/components/space.png'); const AssetGenImage('assets/images/components/space.png');
} }
class $AssetsImagesLinkBoxGen {
const $AssetsImagesLinkBoxGen();
/// File path: assets/images/link_box/info_icon.png
AssetGenImage get infoIcon =>
const AssetGenImage('assets/images/link_box/info_icon.png');
}
class $AssetsImagesScoreGen { class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen(); const $AssetsImagesScoreGen();

@ -120,6 +120,46 @@
"@footerGoogleIOText": { "@footerGoogleIOText": {
"description": "Text shown on the footer which mentions Google I/O" "description": "Text shown on the footer which mentions Google I/O"
}, },
"linkBoxTitle": "Resources",
"@linkBoxTitle": {
"description": "Text shown on the link box title section."
},
"linkBoxMadeWithText": "Made with ",
"@linkBoxMadeWithText": {
"description": "Text shown on the link box which mentions technologies used to build the app."
},
"linkBoxFlutterLinkText": "Flutter",
"@linkBoxFlutterLinkText": {
"description": "Text on the link shown on the link box which navigates to the Flutter page"
},
"linkBoxFirebaseLinkText": "Firebase",
"@linkBoxFirebaseLinkText": {
"description": "Text on the link shown on the link box which navigates to the Firebase page"
},
"linkBoxOpenSourceCode": "Open Source Code",
"@linkBoxOpenSourceCode": {
"description": "Text shown on the link box which redirects to github project"
},
"linkBoxGoogleIOText": "Google I/O",
"@linkBoxGoogleIOText": {
"description": "Text shown on the link box which mentions Google I/O"
},
"linkBoxFlutterGames": "Flutter Games",
"@linkBoxFlutterGames": {
"description": "Text shown on the link box which redirects to flutter games article"
},
"linkBoxHowItsMade": "How it’s made",
"@linkBoxHowItsMade": {
"description": "Text shown on the link box which redirects to Very Good Blog article"
},
"linkBoxTermsOfService": "Terms of Service",
"@linkBoxTermsOfService": {
"description": "Text shown on the link box which redirect to terms of service"
},
"linkBoxPrivacyPolicy": "Privacy Policy",
"@linkBoxPrivacyPolicy": {
"description": "Text shown on the link box which redirect to privacy policy"
},
"loading": "Loading", "loading": "Loading",
"@loading": { "@loading": {
"description": "Text shown to indicate loading times" "description": "Text shown to indicate loading times"

@ -91,7 +91,7 @@ class _SelectedCharacterState extends State<SelectedCharacter>
); );
final animation = spriteSheet.createAnimation( final animation = spriteSheet.createAnimation(
row: 0, row: 0,
stepTime: 1 / 24, stepTime: 1 / 12,
to: spriteSheet.rows * spriteSheet.columns, to: spriteSheet.rows * spriteSheet.columns,
); );
if (_controller != null) _controller?.dispose(); if (_controller != null) _controller?.dispose();

@ -14,10 +14,13 @@ class $AssetsMusicGen {
class $AssetsSfxGen { class $AssetsSfxGen {
const $AssetsSfxGen(); const $AssetsSfxGen();
String get afterLaunch => 'assets/sfx/after_launch.mp3';
String get bumperA => 'assets/sfx/bumper_a.mp3'; String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3'; String get bumperB => 'assets/sfx/bumper_b.mp3';
String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3';
String get google => 'assets/sfx/google.mp3'; String get google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';
String get launcher => 'assets/sfx/launcher.mp3';
} }
class Assets { class Assets {

@ -18,7 +18,13 @@ enum PinballAudio {
backgroundMusic, backgroundMusic,
/// IO Pinball voice over /// IO Pinball voice over
ioPinballVoiceOver ioPinballVoiceOver,
/// Game over
gameOverVoiceOver,
/// Launcher
launcher,
} }
/// Defines the contract of the creation of an [AudioPool]. /// Defines the contract of the creation of an [AudioPool].
@ -30,20 +36,16 @@ typedef CreateAudioPool = Future<AudioPool> Function(
String? prefix, String? prefix,
}); });
/// Function that defines the contract for playing a single /// Defines the contract for playing a single audio.
/// audio
typedef PlaySingleAudio = Future<void> Function(String); typedef PlaySingleAudio = Future<void> Function(String);
/// Function that defines the contract for looping a single /// Defines the contract for looping a single audio.
/// audio
typedef LoopSingleAudio = Future<void> Function(String); typedef LoopSingleAudio = Future<void> Function(String);
/// Function that defines the contract for pre fetching an /// Defines the contract for pre fetching an audio.
/// audio
typedef PreCacheSingleAudio = Future<void> Function(String); typedef PreCacheSingleAudio = Future<void> Function(String);
/// Function that defines the contract for configuring /// Defines the contract for configuring an [AudioCache] instance.
/// an [AudioCache] instance
typedef ConfigureAudioCache = void Function(AudioCache); typedef ConfigureAudioCache = void Function(AudioCache);
abstract class _Audio { abstract class _Audio {
@ -159,11 +161,21 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.google, path: Assets.sfx.google,
), ),
PinballAudio.launcher: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.launcher,
),
PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( PinballAudio.ioPinballVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.ioPinballVoiceOver, path: Assets.sfx.ioPinballVoiceOver,
), ),
PinballAudio.gameOverVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.gameOverVoiceOver,
),
PinballAudio.bumper: _BumperAudio( PinballAudio.bumper: _BumperAudio(
createAudioPool: _createAudioPool, createAudioPool: _createAudioPool,
seed: _seed, seed: _seed,

@ -146,6 +146,15 @@ void main() {
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3', 'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
), ),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/game_over_voice_over.mp3',
),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/launcher.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'), .onCall('packages/pinball_audio/assets/music/background.mp3'),
@ -214,6 +223,18 @@ void main() {
}); });
}); });
group('launcher', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.launcher);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.launcher}'),
).called(1);
});
});
group('ioPinballVoiceOver', () { group('ioPinballVoiceOver', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(player.load());
@ -227,6 +248,19 @@ void main() {
}); });
}); });
group('gameOverVoiceOver', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.gameOverVoiceOver);
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.gameOverVoiceOver}',
),
).called(1);
});
});
group('backgroundMusic', () { group('backgroundMusic', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(player.load());

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 KiB

After

Width:  |  Height:  |  Size: 374 KiB

@ -9,7 +9,7 @@ class BoardBackgroundSpriteComponent extends SpriteComponent
BoardBackgroundSpriteComponent() BoardBackgroundSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -1), position: Vector2(-0.2, 0.1),
) { ) {
zIndex = ZIndexes.boardBackground; zIndex = ZIndexes.boardBackground;
} }

@ -68,7 +68,7 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
_BottomBoundarySpriteComponent() _BottomBoundarySpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-5, 55.6), position: Vector2(-5.2, 55.6),
); );
@override @override

@ -37,46 +37,46 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topEdgeShape = EdgeShape() final topEdgeShape = EdgeShape()
..set( ..set(
Vector2(29.25, -35.27), Vector2(29.05, -35.27),
Vector2(28.4, -34.77), Vector2(28.2, -34.77),
); );
final topCurveShape = BezierCurveShape( final topCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
topEdgeShape.vertex2, topEdgeShape.vertex2,
Vector2(21.35, -28.72), Vector2(21.15, -28.72),
Vector2(23.45, -24.62), Vector2(23.25, -24.62),
], ],
); );
final tunnelTopEdgeShape = EdgeShape() final tunnelTopEdgeShape = EdgeShape()
..set( ..set(
topCurveShape.vertices.last, topCurveShape.vertices.last,
Vector2(30.35, -27.32), Vector2(30.15, -27.32),
); );
final tunnelBottomEdgeShape = EdgeShape() final tunnelBottomEdgeShape = EdgeShape()
..set( ..set(
Vector2(30.75, -23.17), Vector2(30.55, -23.17),
Vector2(25.45, -21.22), Vector2(25.25, -21.22),
); );
final middleEdgeShape = EdgeShape() final middleEdgeShape = EdgeShape()
..set( ..set(
tunnelBottomEdgeShape.vertex2, tunnelBottomEdgeShape.vertex2,
Vector2(27.45, -19.32), Vector2(27.25, -19.32),
); );
final bottomEdgeShape = EdgeShape() final bottomEdgeShape = EdgeShape()
..set( ..set(
middleEdgeShape.vertex2, middleEdgeShape.vertex2,
Vector2(24.65, -15.02), Vector2(24.45, -15.02),
); );
final undersideEdgeShape = EdgeShape() final undersideEdgeShape = EdgeShape()
..set( ..set(
bottomEdgeShape.vertex2, bottomEdgeShape.vertex2,
Vector2(31.75, -13.77), Vector2(31.55, -13.77),
); );
return [ return [
@ -108,7 +108,7 @@ class _DinoTopWallSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex { with HasGameRef, ZIndex {
_DinoTopWallSpriteComponent() _DinoTopWallSpriteComponent()
: super( : super(
position: Vector2(22.75, -38.07), position: Vector2(22.55, -38.07),
) { ) {
zIndex = ZIndexes.dinoTopWall; zIndex = ZIndexes.dinoTopWall;
} }
@ -129,7 +129,7 @@ class _DinoTopWallSpriteComponent extends SpriteComponent
class _DinoTopWallTunnelSpriteComponent extends SpriteComponent class _DinoTopWallTunnelSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex { with HasGameRef, ZIndex {
_DinoTopWallTunnelSpriteComponent() _DinoTopWallTunnelSpriteComponent()
: super(position: Vector2(23.31, -26.01)) { : super(position: Vector2(23.11, -26.01)) {
zIndex = ZIndexes.dinoTopWallTunnel; zIndex = ZIndexes.dinoTopWallTunnel;
} }
@ -162,28 +162,28 @@ class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topEdgeShape = EdgeShape() final topEdgeShape = EdgeShape()
..set( ..set(
Vector2(32.4, -8.8), Vector2(32.2, -8.8),
Vector2(25, -7.7), Vector2(24.8, -7.7),
); );
final topLeftCurveShape = BezierCurveShape( final topLeftCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
topEdgeShape.vertex2, topEdgeShape.vertex2,
Vector2(21.8, -7), Vector2(21.6, -7),
Vector2(29.8, 13.8), Vector2(29.6, 13.8),
], ],
); );
final bottomLeftEdgeShape = EdgeShape() final bottomLeftEdgeShape = EdgeShape()
..set( ..set(
topLeftCurveShape.vertices.last, topLeftCurveShape.vertices.last,
Vector2(31.9, 44.1), Vector2(31.7, 44.1),
); );
final bottomEdgeShape = EdgeShape() final bottomEdgeShape = EdgeShape()
..set( ..set(
bottomLeftEdgeShape.vertex2, bottomLeftEdgeShape.vertex2,
Vector2(37.8, 44.1), Vector2(37.6, 44.1),
); );
return [ return [
@ -219,6 +219,6 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef {
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
position = Vector2(23.8, -9.5); position = Vector2(23.6, -9.5);
} }
} }

@ -18,9 +18,9 @@ class Flapper extends Component {
children: [ children: [
FlapperSpinningBehavior(), FlapperSpinningBehavior(),
], ],
)..initialPosition = Vector2(4, -69.3), )..initialPosition = Vector2(3.8, -69.3),
_FlapperStructure(), _FlapperStructure(),
_FlapperExit()..initialPosition = Vector2(-0.6, -33.8), _FlapperExit()..initialPosition = Vector2(-0.8, -33.8),
_BackSupportSpriteComponent(), _BackSupportSpriteComponent(),
_FrontSupportSpriteComponent(), _FrontSupportSpriteComponent(),
FlapSpriteAnimationComponent(), FlapSpriteAnimationComponent(),
@ -73,14 +73,14 @@ class _FlapperStructure extends BodyComponent with Layered {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final leftEdgeShape = EdgeShape() final leftEdgeShape = EdgeShape()
..set( ..set(
Vector2(1.9, -69.3), Vector2(1.7, -69.3),
Vector2(1.9, -66), Vector2(1.7, -66),
); );
final bottomEdgeShape = EdgeShape() final bottomEdgeShape = EdgeShape()
..set( ..set(
leftEdgeShape.vertex2, leftEdgeShape.vertex2,
Vector2(3.9, -66), Vector2(3.7, -66),
); );
return [ return [
@ -130,7 +130,7 @@ class FlapSpriteAnimationComponent extends SpriteAnimationComponent
FlapSpriteAnimationComponent() FlapSpriteAnimationComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(2.8, -70.7), position: Vector2(2.6, -70.7),
playing: false, playing: false,
) { ) {
zIndex = ZIndexes.flapper; zIndex = ZIndexes.flapper;
@ -173,7 +173,7 @@ class _BackSupportSpriteComponent extends SpriteComponent
_BackSupportSpriteComponent() _BackSupportSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(2.95, -70.6), position: Vector2(2.75, -70.6),
) { ) {
zIndex = ZIndexes.flapperBack; zIndex = ZIndexes.flapperBack;
} }
@ -196,7 +196,7 @@ class _FrontSupportSpriteComponent extends SpriteComponent
_FrontSupportSpriteComponent() _FrontSupportSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(2.9, -67.6), position: Vector2(2.7, -67.7),
) { ) {
zIndex = ZIndexes.flapperFront; zIndex = ZIndexes.flapperFront;
} }

@ -40,22 +40,22 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
final rightStraightShape = EdgeShape() final rightStraightShape = EdgeShape()
..set( ..set(
Vector2(31.4, -61.4), Vector2(31, -61.4),
Vector2(46.5, 68.4), Vector2(46.1, 68.4),
); );
final rightStraightFixtureDef = FixtureDef(rightStraightShape); final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef); fixturesDef.add(rightStraightFixtureDef);
final leftStraightShape = EdgeShape() final leftStraightShape = EdgeShape()
..set( ..set(
Vector2(27.8, -61.4), Vector2(27.4, -61.4),
Vector2(41.5, 68.4), Vector2(41.1, 68.4),
); );
final leftStraightFixtureDef = FixtureDef(leftStraightShape); final leftStraightFixtureDef = FixtureDef(leftStraightShape);
fixturesDef.add(leftStraightFixtureDef); fixturesDef.add(leftStraightFixtureDef);
final topCurveShape = ArcShape( final topCurveShape = ArcShape(
center: Vector2(20.5, -61.1), center: Vector2(20.1, -61.1),
arcRadius: 11, arcRadius: 11,
angle: 1.6, angle: 1.6,
rotation: 0.1, rotation: 0.1,
@ -64,7 +64,7 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
fixturesDef.add(topCurveFixtureDef); fixturesDef.add(topCurveFixtureDef);
final bottomCurveShape = ArcShape( final bottomCurveShape = ArcShape(
center: Vector2(19.3, -60.3), center: Vector2(18.9, -60.3),
arcRadius: 8.5, arcRadius: 8.5,
angle: 1.48, angle: 1.48,
rotation: 0.1, rotation: 0.1,
@ -74,16 +74,16 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
final topStraightShape = EdgeShape() final topStraightShape = EdgeShape()
..set( ..set(
Vector2(3.7, -70.1), Vector2(3.3, -70.1),
Vector2(19.1, -72.1), Vector2(18.7, -72.1),
); );
final topStraightFixtureDef = FixtureDef(topStraightShape); final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef); fixturesDef.add(topStraightFixtureDef);
final bottomStraightShape = EdgeShape() final bottomStraightShape = EdgeShape()
..set( ..set(
Vector2(3.7, -66.9), Vector2(3.3, -66.9),
Vector2(19.1, -68.8), Vector2(18.7, -68.8),
); );
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
fixturesDef.add(bottomStraightFixtureDef); fixturesDef.add(bottomStraightFixtureDef);
@ -113,7 +113,7 @@ class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
position = Vector2(25.65, 0.7); position = Vector2(25.25, 0.7);
} }
} }
@ -131,7 +131,7 @@ class _LaunchRampBackgroundRailingSpriteComponent extends SpriteComponent
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
position = Vector2(25.6, -1.3); position = Vector2(25.2, -1.3);
} }
} }
@ -149,14 +149,14 @@ class _LaunchRampForegroundRailing extends BodyComponent with ZIndex {
final rightStraightShape = EdgeShape() final rightStraightShape = EdgeShape()
..set( ..set(
Vector2(27.6, -57.9), Vector2(27.2, -57.9),
Vector2(38.1, 42.6), Vector2(37.7, 42.6),
); );
final rightStraightFixtureDef = FixtureDef(rightStraightShape); final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef); fixturesDef.add(rightStraightFixtureDef);
final curveShape = ArcShape( final curveShape = ArcShape(
center: Vector2(20.1, -59.3), center: Vector2(19.7, -59.3),
arcRadius: 7.5, arcRadius: 7.5,
angle: 1.8, angle: 1.8,
rotation: -0.13, rotation: -0.13,
@ -166,8 +166,8 @@ class _LaunchRampForegroundRailing extends BodyComponent with ZIndex {
final topStraightShape = EdgeShape() final topStraightShape = EdgeShape()
..set( ..set(
Vector2(3.7, -66.8), Vector2(3.3, -66.8),
Vector2(19.7, -66.8), Vector2(19.3, -66.8),
); );
final topStraightFixtureDef = FixtureDef(topStraightShape); final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef); fixturesDef.add(topStraightFixtureDef);
@ -198,6 +198,6 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
position = Vector2(22.8, 0.5); position = Vector2(22.4, 0.5);
} }
} }

@ -36,8 +36,8 @@ class Multiball extends Component {
Multiball.a({ Multiball.a({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
position: Vector2(-23, 7.5), position: Vector2(-23.3, 7.5),
rotation: -24 * math.pi / 180, rotation: -27 * math.pi / 180,
bloc: MultiballCubit(), bloc: MultiballCubit(),
children: children, children: children,
); );
@ -46,8 +46,8 @@ class Multiball extends Component {
Multiball.b({ Multiball.b({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
position: Vector2(-7.2, -6.2), position: Vector2(-7.65, -6.2),
rotation: -5 * math.pi / 180, rotation: -2 * math.pi / 180,
bloc: MultiballCubit(), bloc: MultiballCubit(),
children: children, children: children,
); );
@ -56,8 +56,8 @@ class Multiball extends Component {
Multiball.c({ Multiball.c({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
position: Vector2(-0.7, -9.3), position: Vector2(-1.1, -9.3),
rotation: 2.7 * math.pi / 180, rotation: 6 * math.pi / 180,
bloc: MultiballCubit(), bloc: MultiballCubit(),
children: children, children: children,
); );
@ -66,8 +66,8 @@ class Multiball extends Component {
Multiball.d({ Multiball.d({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
position: Vector2(15, 7), position: Vector2(14.8, 7),
rotation: 24 * math.pi / 180, rotation: 27 * math.pi / 180,
bloc: MultiballCubit(), bloc: MultiballCubit(),
children: children, children: children,
); );

@ -1,5 +1,6 @@
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_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -13,16 +14,23 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger} /// {@macro plunger}
Plunger({ Plunger({
required this.compressionDistance, required this.compressionDistance,
}) : super(renderBody: false) { }) : super(
renderBody: false,
children: [_PlungerSpriteAnimationGroupComponent()],
) {
zIndex = ZIndexes.plunger; zIndex = ZIndexes.plunger;
layer = Layer.launcher; layer = Layer.launcher;
} }
/// Creates a [Plunger] without any children.
///
/// This can be used for testing [Plunger]'s behaviors in isolation.
@visibleForTesting
Plunger.test({required this.compressionDistance});
/// Distance the plunger can lower. /// Distance the plunger can lower.
final double compressionDistance; final double compressionDistance;
late final _PlungerSpriteAnimationGroupComponent _spriteComponent;
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
@ -78,8 +86,10 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// Set a constant downward velocity on the [Plunger]. /// Set a constant downward velocity on the [Plunger].
void pull() { void pull() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
body.linearVelocity = Vector2(0, 7); body.linearVelocity = Vector2(0, 7);
_spriteComponent.pull(); sprite.pull();
} }
/// Set an upward velocity on the [Plunger]. /// Set an upward velocity on the [Plunger].
@ -87,10 +97,12 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// The velocity's magnitude depends on how far the [Plunger] has been pulled /// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition]. /// from its original [initialPosition].
void release() { void release() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
_pullingDownTime = 0; _pullingDownTime = 0;
final velocity = (initialPosition.y - body.position.y) * 11; final velocity = (initialPosition.y - body.position.y) * 11;
body.linearVelocity = Vector2(0, velocity); body.linearVelocity = Vector2(0, velocity);
_spriteComponent.release(); sprite.release();
} }
@override @override
@ -127,9 +139,6 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await _anchorToJoint(); await _anchorToJoint();
_spriteComponent = _PlungerSpriteAnimationGroupComponent();
await add(_spriteComponent);
} }
} }

@ -13,15 +13,13 @@ class Slingshots extends Component with ZIndex {
: super( : super(
children: [ children: [
Slingshot( Slingshot(
length: 5.64,
angle: -0.017, angle: -0.017,
spritePath: Assets.images.slingshot.upper.keyName, spritePath: Assets.images.slingshot.upper.keyName,
)..initialPosition = Vector2(22.3, -1.58), )..initialPosition = Vector2(22.7, -0.3),
Slingshot( Slingshot(
length: 3.46,
angle: -0.468, angle: -0.468,
spritePath: Assets.images.slingshot.lower.keyName, spritePath: Assets.images.slingshot.lower.keyName,
)..initialPosition = Vector2(24.7, 6.2), )..initialPosition = Vector2(24.6, 6.1),
], ],
) { ) {
zIndex = ZIndexes.slingshots; zIndex = ZIndexes.slingshots;
@ -34,11 +32,9 @@ class Slingshots extends Component with ZIndex {
class Slingshot extends BodyComponent with InitialPosition { class Slingshot extends BodyComponent with InitialPosition {
/// {@macro slingshot} /// {@macro slingshot}
Slingshot({ Slingshot({
required double length,
required double angle, required double angle,
required String spritePath, required String spritePath,
}) : _length = length, }) : _angle = angle,
_angle = angle,
super( super(
children: [ children: [
_SlinghsotSpriteComponent(spritePath, angle: angle), _SlinghsotSpriteComponent(spritePath, angle: angle),
@ -47,29 +43,28 @@ class Slingshot extends BodyComponent with InitialPosition {
renderBody: false, renderBody: false,
); );
final double _length;
final double _angle; final double _angle;
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
const length = 3.46;
const circleRadius = 1.55; const circleRadius = 1.55;
final topCircleShape = CircleShape()..radius = circleRadius; final topCircleShape = CircleShape()..radius = circleRadius;
topCircleShape.position.setValues(0, -_length / 2); topCircleShape.position.setValues(0, -length / 2);
final bottomCircleShape = CircleShape()..radius = circleRadius; final bottomCircleShape = CircleShape()..radius = circleRadius;
bottomCircleShape.position.setValues(0, _length / 2); bottomCircleShape.position.setValues(0, length / 2);
final leftEdgeShape = EdgeShape() final leftEdgeShape = EdgeShape()
..set( ..set(
Vector2(circleRadius, _length / 2), Vector2(circleRadius, length / 2),
Vector2(circleRadius, -_length / 2), Vector2(circleRadius, -length / 2),
); );
final rightEdgeShape = EdgeShape() final rightEdgeShape = EdgeShape()
..set( ..set(
Vector2(-circleRadius, _length / 2), Vector2(-circleRadius, length / 2),
Vector2(-circleRadius, -_length / 2), Vector2(-circleRadius, -length / 2),
); );
return [ return [

@ -32,18 +32,18 @@ class _ComputerBase extends BodyComponent with InitialPosition, ZIndex {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final leftEdge = EdgeShape() final leftEdge = EdgeShape()
..set( ..set(
Vector2(-15.1, -45.9), Vector2(-15.3, -45.9),
Vector2(-15.5, -49.5), Vector2(-15.7, -49.5),
); );
final topEdge = EdgeShape() final topEdge = EdgeShape()
..set( ..set(
leftEdge.vertex2, leftEdge.vertex2,
Vector2(-10.9, -50.5), Vector2(-11.1, -50.5),
); );
final rightEdge = EdgeShape() final rightEdge = EdgeShape()
..set( ..set(
topEdge.vertex2, topEdge.vertex2,
Vector2(-9.2, -47.1), Vector2(-9.4, -47.1),
); );
return [ return [
@ -67,7 +67,7 @@ class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef {
_ComputerBaseSpriteComponent() _ComputerBaseSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-12.24, -48.15), position: Vector2(-12.44, -48.15),
); );
@override @override
@ -89,7 +89,7 @@ class _ComputerTopSpriteComponent extends SpriteComponent
_ComputerTopSpriteComponent() _ComputerTopSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-12.66, -49.37), position: Vector2(-12.86, -49.37),
) { ) {
zIndex = ZIndexes.computerTop; zIndex = ZIndexes.computerTop;
} }
@ -113,9 +113,9 @@ class _ComputerGlowSpriteComponent extends SpriteComponent
_ComputerGlowSpriteComponent() _ComputerGlowSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(4.2, 11), position: Vector2(4, 11),
) { ) {
zIndex = ZIndexes.computerGlow + 4; zIndex = ZIndexes.computerGlow;
} }
@override @override

@ -33,7 +33,7 @@ abstract class ZIndexes {
static const outerBoundary = _above + boardBackground; static const outerBoundary = _above + boardBackground;
static const outerBottomBoundary = _above + rocket; static const outerBottomBoundary = _above + bottomBoundary;
// Bottom Group // Bottom Group
@ -77,7 +77,7 @@ abstract class ZIndexes {
static const computerTop = _above + ballOnBoard; static const computerTop = _above + ballOnBoard;
static const computerGlow = _above + ballOnBoard; static const computerGlow = _above + computerTop;
static const sparkyAnimatronic = _above + spaceshipRampForegroundRailing; static const sparkyAnimatronic = _above + spaceshipRampForegroundRailing;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 254 KiB

@ -1,20 +1,17 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors // ignore_for_file: cascade_invocations, prefer_const_constructors
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.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_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart'; class _TestGame extends Forge2DGame {
@override
class _MockMultiplierCubit extends Mock implements MultiplierCubit {} Future<void> onLoad() async {
images.prefix = '';
void main() { await images.loadAll([
group('Multiplier', () {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiplier.x2.lit.keyName, Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName, Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName, Assets.images.multiplier.x3.lit.keyName,
@ -25,8 +22,16 @@ void main() {
Assets.images.multiplier.x5.dimmed.keyName, Assets.images.multiplier.x5.dimmed.keyName,
Assets.images.multiplier.x6.lit.keyName, Assets.images.multiplier.x6.lit.keyName,
Assets.images.multiplier.x6.dimmed.keyName, Assets.images.multiplier.x6.dimmed.keyName,
]; ]);
final flameTester = FlameTester(() => TestGame(assets)); }
}
class _MockMultiplierCubit extends Mock implements MultiplierCubit {}
void main() {
group('Multiplier', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
late MultiplierCubit bloc; late MultiplierCubit bloc;
setUp(() { setUp(() {
@ -85,7 +90,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'lit when bloc state is lit', 'lit when bloc state is lit',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -116,7 +121,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x2-lit.png'), matchesGoldenFile('../golden/multipliers/x2-lit.png'),
); );
}, },
@ -125,7 +130,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'dimmed when bloc state is dimmed', 'dimmed when bloc state is dimmed',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -156,7 +161,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x2-dimmed.png'), matchesGoldenFile('../golden/multipliers/x2-dimmed.png'),
); );
}, },
@ -169,7 +174,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'lit when bloc state is lit', 'lit when bloc state is lit',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -200,7 +205,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x3-lit.png'), matchesGoldenFile('../golden/multipliers/x3-lit.png'),
); );
}, },
@ -209,7 +214,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'dimmed when bloc state is dimmed', 'dimmed when bloc state is dimmed',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -240,7 +245,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x3-dimmed.png'), matchesGoldenFile('../golden/multipliers/x3-dimmed.png'),
); );
}, },
@ -253,7 +258,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'lit when bloc state is lit', 'lit when bloc state is lit',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -284,7 +289,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x4-lit.png'), matchesGoldenFile('../golden/multipliers/x4-lit.png'),
); );
}, },
@ -293,7 +298,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'dimmed when bloc state is dimmed', 'dimmed when bloc state is dimmed',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -324,7 +329,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x4-dimmed.png'), matchesGoldenFile('../golden/multipliers/x4-dimmed.png'),
); );
}, },
@ -337,7 +342,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'lit when bloc state is lit', 'lit when bloc state is lit',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -368,7 +373,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x5-lit.png'), matchesGoldenFile('../golden/multipliers/x5-lit.png'),
); );
}, },
@ -377,7 +382,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'dimmed when bloc state is dimmed', 'dimmed when bloc state is dimmed',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -408,7 +413,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x5-dimmed.png'), matchesGoldenFile('../golden/multipliers/x5-dimmed.png'),
); );
}, },
@ -421,7 +426,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'lit when bloc state is lit', 'lit when bloc state is lit',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -452,7 +457,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x6-lit.png'), matchesGoldenFile('../golden/multipliers/x6-lit.png'),
); );
}, },
@ -461,7 +466,7 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'dimmed when bloc state is dimmed', 'dimmed when bloc state is dimmed',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
whenListen( whenListen(
bloc, bloc,
@ -492,7 +497,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x6-dimmed.png'), matchesGoldenFile('../golden/multipliers/x6-dimmed.png'),
); );
}, },

@ -14,6 +14,17 @@ void main() {
group('Plunger', () { group('Plunger', () {
const compressionDistance = 0.0; const compressionDistance = 0.0;
test('can be instantiated', () {
expect(
Plunger(compressionDistance: compressionDistance),
isA<Plunger>(),
);
expect(
Plunger.test(compressionDistance: compressionDistance),
isA<Plunger>(),
);
});
flameTester.testGameWidget( flameTester.testGameWidget(
'renders correctly', 'renders correctly',
setUp: (game, tester) async { setUp: (game, tester) async {

@ -3,6 +3,7 @@ library pinball_flame;
export 'src/canvas/canvas.dart'; export 'src/canvas/canvas.dart';
export 'src/component_controller.dart'; export 'src/component_controller.dart';
export 'src/contact_behavior.dart'; export 'src/contact_behavior.dart';
export 'src/flame_provider.dart';
export 'src/keyboard_input_controller.dart'; export 'src/keyboard_input_controller.dart';
export 'src/parent_is_a.dart'; export 'src/parent_is_a.dart';
export 'src/pinball_forge2d_game.dart'; export 'src/pinball_forge2d_game.dart';

@ -26,6 +26,7 @@ class ContactBehavior<T extends BodyComponent> extends Component
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad();
if (_fixturesUserData.isNotEmpty) { if (_fixturesUserData.isNotEmpty) {
for (final fixture in _targetedFixtures) { for (final fixture in _targetedFixtures) {
fixture.userData = _UserData.fromFixture(fixture)..add(this); fixture.userData = _UserData.fromFixture(fixture)..add(this);

@ -0,0 +1,65 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/components.dart';
class FlameProvider<T> extends Component {
FlameProvider.value(
this.provider, {
Iterable<Component>? children,
}) : super(
children: children,
);
final T provider;
}
class MultiFlameProvider extends Component {
MultiFlameProvider({
required List<FlameProvider<dynamic>> providers,
Iterable<Component>? children,
}) : _providers = providers,
_initialChildren = children,
assert(providers.isNotEmpty, 'At least one provider must be given') {
_addProviders();
}
final List<FlameProvider<dynamic>> _providers;
final Iterable<Component>? _initialChildren;
FlameProvider<dynamic>? _lastProvider;
Future<void> _addProviders() async {
final _list = [..._providers];
var current = _list.removeAt(0);
while (_list.isNotEmpty) {
final provider = _list.removeAt(0);
await current.add(provider);
current = provider;
}
await add(_providers.first);
_lastProvider = current;
_initialChildren?.forEach(add);
}
@override
Future<void> add(Component component) async {
if (_lastProvider == null) {
await super.add(component);
}
await _lastProvider?.add(component);
}
}
extension ReadFlameProvider on Component {
T readProvider<T>() {
final providers = ancestors().whereType<FlameProvider<T>>();
assert(
providers.isNotEmpty,
'No FlameProvider<$T> available on the component tree',
);
return providers.first.provider;
}
}

@ -0,0 +1,103 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_flame/pinball_flame.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(FlameGame.new);
group(
'FlameProvider',
() {
test('can be instantiated', () {
expect(
FlameProvider<bool>.value(true),
isA<FlameProvider<bool>>(),
);
});
flameTester.test('can be loaded', (game) async {
final component = FlameProvider<bool>.value(true);
await game.ensureAdd(component);
expect(game.children, contains(component));
});
flameTester.test('adds children', (game) async {
final component = Component();
final provider = FlameProvider<bool>.value(
true,
children: [component],
);
await game.ensureAdd(provider);
expect(provider.children, contains(component));
});
},
);
group('MultiFlameProvider', () {
test('can be instantiated', () {
expect(
MultiFlameProvider(
providers: [
FlameProvider<bool>.value(true),
],
),
isA<MultiFlameProvider>(),
);
});
flameTester.test('adds multiple providers', (game) async {
final provider1 = FlameProvider<bool>.value(true);
final provider2 = FlameProvider<bool>.value(true);
final providers = MultiFlameProvider(
providers: [provider1, provider2],
);
await game.ensureAdd(providers);
expect(providers.children, contains(provider1));
expect(provider1.children, contains(provider2));
});
flameTester.test('adds children under provider', (game) async {
final component = Component();
final provider = FlameProvider<bool>.value(true);
final providers = MultiFlameProvider(
providers: [provider],
children: [component],
);
await game.ensureAdd(providers);
expect(provider.children, contains(component));
});
});
group(
'ReadFlameProvider',
() {
flameTester.test('loads provider', (game) async {
final component = Component();
final provider = FlameProvider<bool>.value(
true,
children: [component],
);
await game.ensureAdd(provider);
expect(component.readProvider<bool>(), isTrue);
});
flameTester.test(
'throws assertionError when no provider is found',
(game) async {
final component = Component();
await game.ensureAdd(component);
expect(
() => component.readProvider<bool>(),
throwsAssertionError,
);
},
);
},
);
}

@ -37,6 +37,13 @@ abstract class PinballTextStyle {
fontFamily: _primaryFontFamily, fontFamily: _primaryFontFamily,
); );
static const headline5 = TextStyle(
color: PinballColors.white,
fontSize: 14,
package: _fontPackage,
fontFamily: _primaryFontFamily,
);
static const subtitle2 = TextStyle( static const subtitle2 = TextStyle(
color: PinballColors.white, color: PinballColors.white,
fontSize: 16, fontSize: 16,
@ -45,7 +52,7 @@ abstract class PinballTextStyle {
); );
static const subtitle1 = TextStyle( static const subtitle1 = TextStyle(
fontSize: 10, fontSize: 12,
fontFamily: _primaryFontFamily, fontFamily: _primaryFontFamily,
package: _fontPackage, package: _fontPackage,
color: PinballColors.yellow, color: PinballColors.yellow,

@ -16,6 +16,7 @@ class PinballTheme {
headline2: PinballTextStyle.headline2, headline2: PinballTextStyle.headline2,
headline3: PinballTextStyle.headline3, headline3: PinballTextStyle.headline3,
headline4: PinballTextStyle.headline4, headline4: PinballTextStyle.headline4,
headline5: PinballTextStyle.headline5,
subtitle1: PinballTextStyle.subtitle1, subtitle1: PinballTextStyle.subtitle1,
subtitle2: PinballTextStyle.subtitle2, subtitle2: PinballTextStyle.subtitle2,
); );

@ -26,9 +26,9 @@ void main() {
expect(style.color, PinballColors.white); expect(style.color, PinballColors.white);
}); });
test('subtitle1 has fontSize 10 and yellow color', () { test('subtitle1 has fontSize 12 and yellow color', () {
const style = PinballTextStyle.subtitle1; const style = PinballTextStyle.subtitle1;
expect(style.fontSize, 10); expect(style.fontSize, 12);
expect(style.color, PinballColors.yellow); expect(style.color, PinballColors.yellow);
}); });

@ -238,7 +238,7 @@ packages:
name: flame_bloc name: flame_bloc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.4.0"
flame_forge2d: flame_forge2d:
dependency: "direct main" dependency: "direct main"
description: description:

@ -15,7 +15,7 @@ dependencies:
firebase_auth: ^3.3.16 firebase_auth: ^3.3.16
firebase_core: ^1.15.0 firebase_core: ^1.15.0
flame: ^1.1.1 flame: ^1.1.1
flame_bloc: ^1.2.0 flame_bloc: ^1.4.0
flame_forge2d: flame_forge2d:
git: git:
url: https://github.com/flame-engine/flame/ url: https://github.com/flame-engine/flame/
@ -60,6 +60,7 @@ flutter:
- assets/images/components/ - assets/images/components/
- assets/images/bonus_animation/ - assets/images/bonus_animation/
- assets/images/score/ - assets/images/score/
- assets/images/link_box/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
@ -7,6 +8,21 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import '../helpers/helpers.dart'; import '../helpers/helpers.dart';
bool _tapTextSpan(RichText richText, String text) {
final isTapped = !richText.text.visitChildren(
(visitor) => _findTextAndTap(visitor, text),
);
return isTapped;
}
bool _findTextAndTap(InlineSpan visitor, String text) {
if (visitor is TextSpan && visitor.text == text) {
(visitor.recognizer as TapGestureRecognizer?)?.onTap?.call();
return false;
}
return true;
}
class _MockUrlLauncher extends Mock class _MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {} implements UrlLauncherPlatform {}
@ -49,7 +65,7 @@ void main() {
).thenAnswer((_) async => true); ).thenAnswer((_) async => true);
await tester.pumpApp(const Footer()); await tester.pumpApp(const Footer());
final flutterTextFinder = find.byWidgetPredicate( final flutterTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && tapTextSpan(widget, 'Flutter'), (widget) => widget is RichText && _tapTextSpan(widget, 'Flutter'),
); );
await tester.tap(flutterTextFinder); await tester.tap(flutterTextFinder);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@ -84,7 +100,7 @@ void main() {
).thenAnswer((_) async => true); ).thenAnswer((_) async => true);
await tester.pumpApp(const Footer()); await tester.pumpApp(const Footer());
final firebaseTextFinder = find.byWidgetPredicate( final firebaseTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && tapTextSpan(widget, 'Firebase'), (widget) => widget is RichText && _tapTextSpan(widget, 'Firebase'),
); );
await tester.tap(firebaseTextFinder); await tester.tap(firebaseTextFinder);
await tester.pumpAndSettle(); await tester.pumpAndSettle();

@ -6,14 +6,24 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(_TestBodyComponent child, {required PinballPlayer player}) {
return ensureAdd(
FlameProvider<PinballPlayer>.value(
player,
children: [
child,
],
),
);
}
}
class _TestBodyComponent extends BodyComponent { class _TestBodyComponent extends BodyComponent {
@override @override
Body createBody() { Body createBody() => world.createBody(BodyDef());
return world.createBody(BodyDef());
}
} }
class _MockPinballPlayer extends Mock implements PinballPlayer {} class _MockPinballPlayer extends Mock implements PinballPlayer {}
@ -23,12 +33,10 @@ class _MockContact extends Mock implements Contact {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
group('BumperNoisyBehavior', () {}); group('BumperNoiseBehavior', () {});
late PinballPlayer player; late PinballPlayer player;
final flameTester = FlameTester( final flameTester = FlameTester(_TestGame.new);
() => EmptyPinballTestGame(player: player),
);
setUp(() { setUp(() {
player = _MockPinballPlayer(); player = _MockPinballPlayer();
@ -37,9 +45,9 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'plays bumper sound', 'plays bumper sound',
setUp: (game, _) async { setUp: (game, _) async {
final behavior = BumperNoisyBehavior(); final behavior = BumperNoiseBehavior();
final parent = _TestBodyComponent(); final parent = _TestBodyComponent();
await game.ensureAdd(parent); await game.pump(parent, player: player);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);
behavior.beginContact(Object(), _MockContact()); behavior.beginContact(Object(), _MockContact());
}, },

@ -0,0 +1,143 @@
// ignore_for_file: cascade_invocations
import 'package:flame/game.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/behaviors/camera_focusing_behavior.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group(
'CameraFocusingBehavior',
() {
final flameTester = FlameTester(FlameGame.new);
test('can be instantiated', () {
expect(
CameraFocusingBehavior(),
isA<CameraFocusingBehavior>(),
);
});
flameTester.test('loads', (game) async {
late final behavior = CameraFocusingBehavior();
await game.ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [behavior],
),
);
expect(game.descendants(), contains(behavior));
});
flameTester.test(
'changes focus when loaded',
(game) async {
final behavior = CameraFocusingBehavior();
final previousZoom = game.camera.zoom;
expect(game.camera.follow, isNull);
await game.ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [behavior],
),
);
expect(game.camera.follow, isNotNull);
expect(game.camera.zoom, isNot(equals(previousZoom)));
},
);
flameTester.test(
'listenWhen only listens when status changes',
(game) async {
final behavior = CameraFocusingBehavior();
const waiting = GameState.initial();
final playing =
const GameState.initial().copyWith(status: GameStatus.playing);
final gameOver =
const GameState.initial().copyWith(status: GameStatus.gameOver);
expect(behavior.listenWhen(waiting, waiting), isFalse);
expect(behavior.listenWhen(waiting, playing), isTrue);
expect(behavior.listenWhen(waiting, gameOver), isTrue);
expect(behavior.listenWhen(playing, playing), isFalse);
expect(behavior.listenWhen(playing, waiting), isTrue);
expect(behavior.listenWhen(playing, gameOver), isTrue);
expect(behavior.listenWhen(gameOver, gameOver), isFalse);
expect(behavior.listenWhen(gameOver, waiting), isTrue);
expect(behavior.listenWhen(gameOver, playing), isTrue);
},
);
group('onNewState', () {
flameTester.test(
'zooms when started playing',
(game) async {
final playing =
const GameState.initial().copyWith(status: GameStatus.playing);
final behavior = CameraFocusingBehavior();
await game.ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [behavior],
),
);
behavior.onNewState(playing);
final previousPosition = game.camera.position.clone();
await game.ready();
final zoom = behavior.children.whereType<CameraZoom>().single;
game.update(zoom.controller.duration!);
game.update(0);
expect(zoom.controller.completed, isTrue);
expect(
game.camera.position,
isNot(equals(previousPosition)),
);
},
);
flameTester.test(
'zooms when game is over',
(game) async {
final playing = const GameState.initial().copyWith(
status: GameStatus.gameOver,
);
final behavior = CameraFocusingBehavior();
await game.ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [behavior],
),
);
behavior.onNewState(playing);
final previousPosition = game.camera.position.clone();
await game.ready();
final zoom = behavior.children.whereType<CameraZoom>().single;
game.update(zoom.controller.duration!);
game.update(0);
expect(zoom.controller.completed, isTrue);
expect(
game.camera.position,
isNot(equals(previousPosition)),
);
},
);
});
},
);
}

@ -1,6 +1,6 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -10,7 +10,29 @@ 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';
import '../../helpers/helpers.dart'; class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
]);
}
Future<void> pump(BodyComponent child, {GameBloc? gameBloc}) {
return ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc ?? GameBloc(),
children: [
ZCanvasComponent(children: [child])
],
),
);
}
}
class _TestBodyComponent extends BodyComponent { class _TestBodyComponent extends BodyComponent {
@override @override
@ -27,18 +49,13 @@ class _MockContact extends Mock implements Contact {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
];
late GameBloc bloc; late GameBloc bloc;
late Ball ball; late Ball ball;
late BodyComponent parent; late BodyComponent parent;
setUp(() { setUp(() {
bloc = _MockGameBloc();
ball = _MockBall(); ball = _MockBall();
final ballBody = _MockBody(); final ballBody = _MockBody();
when(() => ball.body).thenReturn(ballBody); when(() => ball.body).thenReturn(ballBody);
@ -47,23 +64,7 @@ void main() {
parent = _TestBodyComponent(); parent = _TestBodyComponent();
}); });
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>( final flameBlocTester = FlameTester(_TestGame.new);
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () {
bloc = _MockGameBloc();
const state = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 3,
bonusHistory: [],
status: GameStatus.playing,
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
assets: assets,
);
group('ScoringBehavior', () { group('ScoringBehavior', () {
test('can be instantiated', () { test('can be instantiated', () {
@ -76,16 +77,16 @@ void main() {
); );
}); });
flameBlocTester.testGameWidget( flameBlocTester.test(
'can be loaded', 'can be loaded',
setUp: (game, tester) async { (game) async {
final canvas = ZCanvasComponent(children: [parent]); await game.pump(parent);
final behavior = ScoringBehavior( final behavior = ScoringBehavior(
points: Points.fiveThousand, points: Points.fiveThousand,
position: Vector2.zero(), position: Vector2.zero(),
); );
await parent.add(behavior); await parent.ensureAdd(behavior);
await game.ensureAdd(canvas);
expect( expect(
parent.firstChild<ScoringBehavior>(), parent.firstChild<ScoringBehavior>(),
@ -94,13 +95,12 @@ void main() {
}, },
); );
flameBlocTester.testGameWidget( flameBlocTester.test(
'emits Scored event with points when added', 'emits Scored event with points when added',
setUp: (game, tester) async { (game) async {
const points = Points.oneMillion; await game.pump(parent, gameBloc: bloc);
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
const points = Points.oneMillion;
final behavior = ScoringBehavior( final behavior = ScoringBehavior(
points: points, points: points,
position: Vector2(0, 0), position: Vector2(0, 0),
@ -115,11 +115,10 @@ void main() {
}, },
); );
flameBlocTester.testGameWidget( flameBlocTester.test(
'correctly renders text', 'correctly renders text',
setUp: (game, tester) async { (game) async {
final canvas = ZCanvasComponent(children: [parent]); await game.pump(parent);
await game.ensureAdd(canvas);
const points = Points.oneMillion; const points = Points.oneMillion;
final position = Vector2.all(1); final position = Vector2.all(1);
@ -145,8 +144,8 @@ void main() {
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'is removed after duration', 'is removed after duration',
setUp: (game, tester) async { setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]); await game.onLoad();
await game.ensureAdd(canvas); await game.pump(parent);
const duration = 2.0; const duration = 2.0;
final behavior = ScoringBehavior( final behavior = ScoringBehavior(
@ -173,8 +172,8 @@ void main() {
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'beginContact adds a ScoringBehavior', 'beginContact adds a ScoringBehavior',
setUp: (game, tester) async { setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]); await game.onLoad();
await game.ensureAdd(canvas); await game.pump(parent);
final behavior = ScoringContactBehavior(points: Points.oneMillion); final behavior = ScoringContactBehavior(points: Points.oneMillion);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);
@ -192,8 +191,8 @@ void main() {
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
"beginContact positions text at contact's position", "beginContact positions text at contact's position",
setUp: (game, tester) async { setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]); await game.onLoad();
await game.ensureAdd(canvas); await game.pump(parent);
final behavior = ScoringContactBehavior(points: Points.oneMillion); final behavior = ScoringContactBehavior(points: Points.oneMillion);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);

@ -1,17 +1,19 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/behaviors/bumper_noisy_behavior.dart'; import 'package:pinball/game/behaviors/bumper_noise_behavior.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.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';
import '../../../helpers/helpers.dart'; class _TestGame extends Forge2DGame {
@override
void main() { Future<void> onLoad() async {
TestWidgetsFlutterBinding.ensureInitialized(); images.prefix = '';
final assets = [ await images.loadAll([
Assets.images.android.spaceship.saucer.keyName, Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName, Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName, Assets.images.android.spaceship.lightBeam.keyName,
@ -33,24 +35,36 @@ void main() {
Assets.images.android.bumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName, Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName, Assets.images.android.bumper.cow.dimmed.keyName,
]; ]);
}
group('AndroidAcres', () { Future<void> pump(AndroidAcres child) async {
final flameTester = FlameTester( await ensureAdd(
() => EmptyPinballTestGame(assets: assets), FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [child],
),
); );
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('AndroidAcres', () {
final flameTester = FlameTester(_TestGame.new);
flameTester.test('loads correctly', (game) async { flameTester.test('loads correctly', (game) async {
final component = AndroidAcres(); final component = AndroidAcres();
await game.ensureAdd(component); await game.pump(component);
expect(game.contains(component), isTrue); expect(game.descendants(), contains(component));
}); });
group('loads', () { group('loads', () {
flameTester.test( flameTester.test(
'an AndroidSpaceship', 'an AndroidSpaceship',
(game) async { (game) async {
await game.ensureAdd(AndroidAcres()); await game.pump(AndroidAcres());
expect( expect(
game.descendants().whereType<AndroidSpaceship>().length, game.descendants().whereType<AndroidSpaceship>().length,
equals(1), equals(1),
@ -61,7 +75,7 @@ void main() {
flameTester.test( flameTester.test(
'an AndroidAnimatronic', 'an AndroidAnimatronic',
(game) async { (game) async {
await game.ensureAdd(AndroidAcres()); await game.pump(AndroidAcres());
expect( expect(
game.descendants().whereType<AndroidAnimatronic>().length, game.descendants().whereType<AndroidAnimatronic>().length,
equals(1), equals(1),
@ -72,7 +86,7 @@ void main() {
flameTester.test( flameTester.test(
'a SpaceshipRamp', 'a SpaceshipRamp',
(game) async { (game) async {
await game.ensureAdd(AndroidAcres()); await game.pump(AndroidAcres());
expect( expect(
game.descendants().whereType<SpaceshipRamp>().length, game.descendants().whereType<SpaceshipRamp>().length,
equals(1), equals(1),
@ -83,7 +97,7 @@ void main() {
flameTester.test( flameTester.test(
'a SpaceshipRail', 'a SpaceshipRail',
(game) async { (game) async {
await game.ensureAdd(AndroidAcres()); await game.pump(AndroidAcres());
expect( expect(
game.descendants().whereType<SpaceshipRail>().length, game.descendants().whereType<SpaceshipRail>().length,
equals(1), equals(1),
@ -94,7 +108,7 @@ void main() {
flameTester.test( flameTester.test(
'three AndroidBumper', 'three AndroidBumper',
(game) async { (game) async {
await game.ensureAdd(AndroidAcres()); await game.pump(AndroidAcres());
expect( expect(
game.descendants().whereType<AndroidBumper>().length, game.descendants().whereType<AndroidBumper>().length,
equals(3), equals(3),
@ -103,13 +117,13 @@ void main() {
); );
flameTester.test( flameTester.test(
'three AndroidBumpers with BumperNoisyBehavior', 'three AndroidBumpers with BumperNoiseBehavior',
(game) async { (game) async {
await game.ensureAdd(AndroidAcres()); await game.pump(AndroidAcres());
final bumpers = game.descendants().whereType<AndroidBumper>(); final bumpers = game.descendants().whereType<AndroidBumper>();
for (final bumper in bumpers) { for (final bumper in bumpers) {
expect( expect(
bumper.firstChild<BumperNoisyBehavior>(), bumper.firstChild<BumperNoiseBehavior>(),
isNotNull, isNotNull,
); );
} }
@ -119,7 +133,7 @@ void main() {
flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async { flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async {
final androidAcres = AndroidAcres(); final androidAcres = AndroidAcres();
await game.ensureAdd(androidAcres); await game.pump(androidAcres);
expect( expect(
androidAcres.children.whereType<AndroidSpaceshipBonusBehavior>().single, androidAcres.children.whereType<AndroidSpaceshipBonusBehavior>().single,
isNotNull, isNotNull,

@ -1,7 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.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';
@ -9,13 +9,11 @@ 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';
import '../../../../helpers/helpers.dart'; class _TestGame extends Forge2DGame {
@override
class _MockGameBloc extends Mock implements GameBloc {} Future<void> onLoad() async {
images.prefix = '';
void main() { await images.loadAll([
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.spaceship.saucer.keyName, Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName, Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName, Assets.images.android.spaceship.lightBeam.keyName,
@ -37,27 +35,40 @@ void main() {
Assets.images.android.bumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName, Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName, Assets.images.android.bumper.cow.dimmed.keyName,
]; ]);
}
Future<void> pump(
AndroidAcres child, {
required GameBloc gameBloc,
}) async {
// Not needed once https://github.com/flame-engine/flame/issues/1607
// is fixed
await onLoad();
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
children: [child],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('AndroidSpaceshipBonusBehavior', () { group('AndroidSpaceshipBonusBehavior', () {
late GameBloc gameBloc; late GameBloc gameBloc;
setUp(() { setUp(() {
gameBloc = _MockGameBloc(); gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameTester = FlameTester(_TestGame.new);
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
flameBlocTester.testGameWidget( flameTester.testGameWidget(
'adds GameBonus.androidSpaceship to the game ' 'adds GameBonus.androidSpaceship to the game '
'when android spacehship has a bonus', 'when android spacehship has a bonus',
setUp: (game, tester) async { setUp: (game, tester) async {
@ -66,7 +77,10 @@ void main() {
final androidSpaceship = AndroidSpaceship(position: Vector2.zero()); final androidSpaceship = AndroidSpaceship(position: Vector2.zero());
await parent.add(androidSpaceship); await parent.add(androidSpaceship);
await game.ensureAdd(parent); await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);
androidSpaceship.bloc.onBallEntered(); androidSpaceship.bloc.onBallEntered();

@ -0,0 +1,142 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/ball_spawning_behavior.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.load(theme.Assets.images.dash.ball.keyName);
}
Future<void> pump(
Iterable<Component> children, {
GameBloc? gameBloc,
}) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc ?? GameBloc(),
children: [
FlameProvider<theme.CharacterTheme>.value(
const theme.DashTheme(),
children: children,
),
],
),
);
}
}
class _MockGameState extends Mock implements GameState {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group(
'BallSpawningBehavior',
() {
final flameTester = FlameTester(_TestGame.new);
test('can be instantiated', () {
expect(
BallSpawningBehavior(),
isA<BallSpawningBehavior>(),
);
});
flameTester.test(
'loads',
(game) async {
final behavior = BallSpawningBehavior();
await game.pump([behavior]);
expect(game.descendants(), contains(behavior));
},
);
group('listenWhen', () {
test(
'never listens when new state not playing',
() {
final waiting = const GameState.initial()
..copyWith(status: GameStatus.waiting);
final gameOver = const GameState.initial()
..copyWith(status: GameStatus.gameOver);
final behavior = BallSpawningBehavior();
expect(behavior.listenWhen(_MockGameState(), waiting), isFalse);
expect(behavior.listenWhen(_MockGameState(), gameOver), isFalse);
},
);
test(
'listens when started playing',
() {
final waiting =
const GameState.initial().copyWith(status: GameStatus.waiting);
final playing =
const GameState.initial().copyWith(status: GameStatus.playing);
final behavior = BallSpawningBehavior();
expect(behavior.listenWhen(waiting, playing), isTrue);
},
);
test(
'listens when lost rounds',
() {
final playing1 = const GameState.initial().copyWith(
status: GameStatus.playing,
rounds: 2,
);
final playing2 = const GameState.initial().copyWith(
status: GameStatus.playing,
rounds: 1,
);
final behavior = BallSpawningBehavior();
expect(behavior.listenWhen(playing1, playing2), isTrue);
},
);
test(
"doesn't listen when didn't lose any rounds",
() {
final playing = const GameState.initial().copyWith(
status: GameStatus.playing,
rounds: 2,
);
final behavior = BallSpawningBehavior();
expect(behavior.listenWhen(playing, playing), isFalse);
},
);
});
flameTester.test(
'onNewState adds a ball',
(game) async {
final behavior = BallSpawningBehavior();
await game.pump([
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
]);
expect(game.descendants().whereType<Ball>(), isEmpty);
behavior.onNewState(_MockGameState());
await game.ready();
expect(game.descendants().whereType<Ball>(), isNotEmpty);
},
);
},
);
}

@ -3,6 +3,8 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.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';
@ -12,18 +14,11 @@ 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';
import '../../../../helpers/helpers.dart'; class _TestGame extends Forge2DGame {
@override
class _MockGameBloc extends Mock implements GameBloc {} Future<void> onLoad() async {
images.prefix = '';
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} await images.loadAll([
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName, Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName, Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName, Assets.images.android.ramp.railingBackground.keyName,
@ -37,7 +32,33 @@ void main() {
Assets.images.android.rail.main.keyName, Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName, Assets.images.android.rail.exit.keyName,
Assets.images.score.oneMillion.keyName, Assets.images.score.oneMillion.keyName,
]; ]);
}
Future<void> pump(
SpaceshipRamp child, {
required GameBloc gameBloc,
}) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
children: [
ZCanvasComponent(children: [child]),
],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RampBonusBehavior', () { group('RampBonusBehavior', () {
const shotPoints = Points.oneMillion; const shotPoints = Points.oneMillion;
@ -46,22 +67,13 @@ void main() {
setUp(() { setUp(() {
gameBloc = _MockGameBloc(); gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameTester = FlameTester(_TestGame.new);
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
flameBlocTester.testGameWidget( flameTester.test(
'when hits are multiples of 10 times adds a ScoringBehavior', 'when hits are multiples of 10 times adds a ScoringBehavior',
setUp: (game, tester) async { (game) async {
final bloc = _MockSpaceshipRampCubit(); final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>(); final streamController = StreamController<SpaceshipRampState>();
whenListen( whenListen(
@ -69,14 +81,13 @@ void main() {
streamController.stream, streamController.stream,
initialState: SpaceshipRampState(hits: 9), initialState: SpaceshipRampState(hits: 9),
); );
final behavior = RampBonusBehavior( final behavior = RampBonusBehavior(points: shotPoints);
points: shotPoints, final parent = SpaceshipRamp.test(bloc: bloc);
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent])); await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10)); streamController.add(SpaceshipRampState(hits: 10));
@ -88,9 +99,9 @@ void main() {
}, },
); );
flameBlocTester.testGameWidget( flameTester.test(
"when hits are not multiple of 10 times doesn't add any ScoringBehavior", "when hits are not multiple of 10 times doesn't add any ScoringBehavior",
setUp: (game, tester) async { (game) async {
final bloc = _MockSpaceshipRampCubit(); final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>(); final streamController = StreamController<SpaceshipRampState>();
whenListen( whenListen(
@ -98,14 +109,13 @@ void main() {
streamController.stream, streamController.stream,
initialState: SpaceshipRampState.initial(), initialState: SpaceshipRampState.initial(),
); );
final behavior = RampBonusBehavior( final behavior = RampBonusBehavior(points: shotPoints);
points: shotPoints, final parent = SpaceshipRamp.test(bloc: bloc);
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent])); await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1)); streamController.add(SpaceshipRampState(hits: 1));
@ -117,9 +127,9 @@ void main() {
}, },
); );
flameBlocTester.testGameWidget( flameTester.test(
'closes subscription when removed', 'closes subscription when removed',
setUp: (game, tester) async { (game) async {
final bloc = _MockSpaceshipRampCubit(); final bloc = _MockSpaceshipRampCubit();
whenListen( whenListen(
bloc, bloc,
@ -135,11 +145,12 @@ void main() {
points: shotPoints, points: shotPoints,
subscription: subscription, subscription: subscription,
); );
final parent = SpaceshipRamp.test( final parent = SpaceshipRamp.test(bloc: bloc);
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent])); await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);
parent.remove(behavior); parent.remove(behavior);

@ -3,6 +3,8 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.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';
@ -12,18 +14,11 @@ 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';
import '../../../../helpers/helpers.dart'; class _TestGame extends Forge2DGame {
@override
class _MockGameBloc extends Mock implements GameBloc {} Future<void> onLoad() async {
images.prefix = '';
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} await images.loadAll([
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName, Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName, Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName, Assets.images.android.ramp.railingBackground.keyName,
@ -37,7 +32,33 @@ void main() {
Assets.images.android.rail.main.keyName, Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName, Assets.images.android.rail.exit.keyName,
Assets.images.score.fiveThousand.keyName, Assets.images.score.fiveThousand.keyName,
]; ]);
}
Future<void> pump(
SpaceshipRamp child, {
required GameBloc gameBloc,
}) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
children: [
ZCanvasComponent(children: [child]),
],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RampShotBehavior', () { group('RampShotBehavior', () {
const shotPoints = Points.fiveThousand; const shotPoints = Points.fiveThousand;
@ -46,23 +67,14 @@ void main() {
setUp(() { setUp(() {
gameBloc = _MockGameBloc(); gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameTester(_TestGame.new);
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
flameBlocTester.testGameWidget( flameBlocTester.test(
'when hits are not multiple of 10 times ' 'when hits are not multiple of 10 times '
'increases multiplier and adds a ScoringBehavior', 'increases multiplier and adds a ScoringBehavior',
setUp: (game, tester) async { (game) async {
final bloc = _MockSpaceshipRampCubit(); final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>(); final streamController = StreamController<SpaceshipRampState>();
whenListen( whenListen(
@ -70,14 +82,13 @@ void main() {
streamController.stream, streamController.stream,
initialState: SpaceshipRampState.initial(), initialState: SpaceshipRampState.initial(),
); );
final behavior = RampShotBehavior( final behavior = RampShotBehavior(points: shotPoints);
points: shotPoints, final parent = SpaceshipRamp.test(bloc: bloc);
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent])); await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1)); streamController.add(SpaceshipRampState(hits: 1));
@ -90,10 +101,10 @@ void main() {
}, },
); );
flameBlocTester.testGameWidget( flameBlocTester.test(
'when hits multiple of 10 times ' 'when hits multiple of 10 times '
"doesn't increase multiplier, neither ScoringBehavior", "doesn't increase multiplier, neither ScoringBehavior",
setUp: (game, tester) async { (game) async {
final bloc = _MockSpaceshipRampCubit(); final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>(); final streamController = StreamController<SpaceshipRampState>();
whenListen( whenListen(
@ -108,7 +119,10 @@ void main() {
bloc: bloc, bloc: bloc,
); );
await game.ensureAdd(ZCanvasComponent(children: [parent])); await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10)); streamController.add(SpaceshipRampState(hits: 10));
@ -121,9 +135,9 @@ void main() {
}, },
); );
flameBlocTester.testGameWidget( flameBlocTester.test(
'closes subscription when removed', 'closes subscription when removed',
setUp: (game, tester) async { (game) async {
final bloc = _MockSpaceshipRampCubit(); final bloc = _MockSpaceshipRampCubit();
whenListen( whenListen(
bloc, bloc,
@ -143,7 +157,10 @@ void main() {
bloc: bloc, bloc: bloc,
); );
await game.ensureAdd(ZCanvasComponent(children: [parent])); await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior); await parent.ensureAdd(behavior);
parent.remove(behavior); parent.remove(behavior);

@ -3,7 +3,9 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -15,9 +17,39 @@ import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.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_theme/pinball_theme.dart' as theme; import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../helpers/helpers.dart'; class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents {
final character = theme.DashTheme();
@override
Color backgroundColor() => Colors.transparent;
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
character.leaderboardIcon.keyName,
Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName,
]);
}
Future<void> pump(Backbox component) {
return ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [
FlameProvider.value(
_MockAppLocalizations(),
children: [component],
),
],
),
);
}
}
class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
@override @override
@ -44,6 +76,9 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
@override @override
String get name => ''; String get name => '';
@override
String get rank => '';
@override @override
String get enterInitials => ''; String get enterInitials => '';
@ -65,18 +100,8 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
const character = theme.AndroidTheme();
final assets = [ final flameTester = FlameTester(_TestGame.new);
character.leaderboardIcon.keyName,
Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(
assets: assets,
l10n: _MockAppLocalizations(),
),
);
late BackboxBloc bloc; late BackboxBloc bloc;
@ -84,7 +109,7 @@ void main() {
bloc = _MockBackboxBloc(); bloc = _MockBackboxBloc();
whenListen( whenListen(
bloc, bloc,
Stream.value(LoadingState()), Stream<BackboxState>.empty(),
initialState: LoadingState(), initialState: LoadingState(),
); );
}); });
@ -94,27 +119,36 @@ void main() {
'loads correctly', 'loads correctly',
(game) async { (game) async {
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(bloc: bloc);
await game.ensureAdd(backbox); await game.pump(backbox);
expect(game.descendants(), contains(backbox));
},
);
expect(game.children, contains(backbox)); flameTester.test(
'adds LeaderboardRequested when loaded',
(game) async {
final backbox = Backbox.test(bloc: bloc);
await game.pump(backbox);
verify(() => bloc.add(LeaderboardRequested())).called(1);
}, },
); );
flameTester.testGameWidget( flameTester.testGameWidget(
'renders correctly', 'renders correctly',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.onLoad();
game.camera game.camera
..followVector2(Vector2(0, -130)) ..followVector2(Vector2(0, -130))
..zoom = 6; ..zoom = 6;
await game.ensureAdd( await game.pump(
Backbox.test(bloc: bloc), Backbox.test(bloc: bloc),
); );
await tester.pump(); await tester.pump();
}, },
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(
find.byGame<EmptyPinballTestGame>(), find.byGame<_TestGame>(),
matchesGoldenFile('../golden/backbox.png'), matchesGoldenFile('../golden/backbox.png'),
); );
}, },
@ -128,10 +162,10 @@ void main() {
leaderboardRepository: _MockLeaderboardRepository(), leaderboardRepository: _MockLeaderboardRepository(),
), ),
); );
await game.ensureAdd(backbox); await game.pump(backbox);
backbox.requestInitials( backbox.requestInitials(
score: 0, score: 0,
character: character, character: game.character,
); );
await game.ready(); await game.ready();
@ -148,15 +182,15 @@ void main() {
final bloc = _MockBackboxBloc(); final bloc = _MockBackboxBloc();
final state = InitialsFormState( final state = InitialsFormState(
score: 10, score: 10,
character: theme.AndroidTheme(), character: game.character,
); );
whenListen( whenListen(
bloc, bloc,
Stream.value(state), Stream<BackboxState>.empty(),
initialState: state, initialState: state,
); );
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(bloc: bloc);
await game.ensureAdd(backbox); await game.pump(backbox);
game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {}); game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {});
verify( verify(
@ -164,7 +198,7 @@ void main() {
PlayerInitialsSubmitted( PlayerInitialsSubmitted(
score: 10, score: 10,
initials: 'AAA', initials: 'AAA',
character: theme.AndroidTheme(), character: game.character,
), ),
), ),
).called(1); ).called(1);
@ -176,11 +210,11 @@ void main() {
(game) async { (game) async {
whenListen( whenListen(
bloc, bloc,
Stream.value(InitialsSuccessState()), Stream<BackboxState>.empty(),
initialState: InitialsSuccessState(), initialState: InitialsSuccessState(),
); );
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(bloc: bloc);
await game.ensureAdd(backbox); await game.pump(backbox);
expect( expect(
game game
@ -197,11 +231,11 @@ void main() {
(game) async { (game) async {
whenListen( whenListen(
bloc, bloc,
Stream.value(InitialsFailureState()), Stream<BackboxState>.empty(),
initialState: InitialsFailureState(), initialState: InitialsFailureState(),
); );
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(bloc: bloc);
await game.ensureAdd(backbox); await game.pump(backbox);
expect( expect(
game game
@ -213,6 +247,25 @@ void main() {
}, },
); );
flameTester.test(
'adds LeaderboardDisplay on LeaderboardSuccessState',
(game) async {
whenListen(
bloc,
Stream<BackboxState>.empty(),
initialState: LeaderboardSuccessState(entries: const []),
);
final backbox = Backbox.test(bloc: bloc);
await game.pump(backbox);
expect(
game.descendants().whereType<LeaderboardDisplay>().length,
equals(1),
);
},
);
flameTester.test( flameTester.test(
'closes the subscription when it is removed', 'closes the subscription when it is removed',
(game) async { (game) async {
@ -224,7 +277,7 @@ void main() {
); );
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(bloc: bloc);
await game.ensureAdd(backbox); await game.pump(backbox);
backbox.removeFromParent(); backbox.removeFromParent();
await game.ready(); await game.ready();

@ -88,5 +88,41 @@ void main() {
], ],
); );
}); });
group('LeaderboardRequested', () {
blocTest<BackboxBloc, BackboxState>(
'adds [LoadingState, LeaderboardSuccessState] when request succeeds',
setUp: () {
leaderboardRepository = _MockLeaderboardRepository();
when(
() => leaderboardRepository.fetchTop10Leaderboard(),
).thenAnswer(
(_) async => [LeaderboardEntryData.empty],
);
},
build: () => BackboxBloc(leaderboardRepository: leaderboardRepository),
act: (bloc) => bloc.add(LeaderboardRequested()),
expect: () => [
LoadingState(),
LeaderboardSuccessState(entries: const [LeaderboardEntryData.empty]),
],
);
blocTest<BackboxBloc, BackboxState>(
'adds [LoadingState, LeaderboardFailureState] when request fails',
setUp: () {
leaderboardRepository = _MockLeaderboardRepository();
when(
() => leaderboardRepository.fetchTop10Leaderboard(),
).thenThrow(Exception('Error'));
},
build: () => BackboxBloc(leaderboardRepository: leaderboardRepository),
act: (bloc) => bloc.add(LeaderboardRequested()),
expect: () => [
LoadingState(),
LeaderboardFailureState(),
],
);
});
}); });
} }

@ -122,5 +122,15 @@ void main() {
); );
}); });
}); });
group('LeaderboardRequested', () {
test('can be instantiated', () {
expect(LeaderboardRequested(), isNotNull);
});
test('supports value comparison', () {
expect(LeaderboardRequested(), equals(LeaderboardRequested()));
});
});
}); });
} }

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

Loading…
Cancel
Save