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';

@ -1,15 +1,13 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/components.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_flame/pinball_flame.dart';
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> {
class BumperNoiseBehavior extends ContactBehavior {
@override
void beginContact(Object other, Contact 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/effects.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';
@ -12,7 +13,8 @@ import 'package:pinball_flame/pinball_flame.dart';
///
/// The behavior removes itself after the duration.
/// {@endtemplate}
class ScoringBehavior extends Component with HasGameRef<PinballGame> {
class ScoringBehavior extends Component
with HasGameRef, FlameBlocReader<GameBloc, GameState> {
/// {@macto scoring_behavior}
ScoringBehavior({
required Points points,
@ -39,7 +41,8 @@ class ScoringBehavior extends Component with HasGameRef<PinballGame> {
@override
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;
await canvas.add(
ScoreComponent(
@ -54,8 +57,7 @@ class ScoringBehavior extends Component with HasGameRef<PinballGame> {
/// {@template scoring_contact_behavior}
/// Adds points to the score when the [Ball] contacts the [parent].
/// {@endtemplate}
class ScoringContactBehavior extends ContactBehavior
with HasGameRef<PinballGame> {
class ScoringContactBehavior extends ContactBehavior {
/// {@macro scoring_contact_behavior}
ScoringContactBehavior({
required Points points,

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

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

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

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

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

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

@ -18,6 +18,7 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
super(LoadingState()) {
on<PlayerInitialsRequested>(_onPlayerInitialsRequested);
on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted);
on<LeaderboardRequested>(_onLeaderboardRequested);
}
final LeaderboardRepository _leaderboardRepository;
@ -53,4 +54,20 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
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
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 => [];
}
/// {@template leaderboard_success_state}
/// State when the leaderboard was successfully loaded.
/// {@endtemplate}
class LeaderboardSuccessState extends BackboxState {
/// {@macro leaderboard_success_state}
const LeaderboardSuccessState({required this.entries});
/// Current entries
final List<LeaderboardEntryData> entries;
@override
List<Object?> get props => [];
List<Object?> get props => [entries];
}
/// State when the leaderboard failed to load.

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

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

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

@ -15,8 +15,7 @@ final _bodyTextPaint = TextPaint(
/// {@template initials_submission_success_display}
/// [Backbox] display for initials successfully submitted.
/// {@endtemplate}
class InitialsSubmissionSuccessDisplay extends TextComponent
with HasGameRef<PinballGame> {
class InitialsSubmissionSuccessDisplay extends TextComponent {
@override
Future<void> onLoad() async {
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: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_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
final _bodyTextPaint = TextPaint(
@ -15,7 +16,7 @@ final _bodyTextPaint = TextPaint(
/// {@template loading_display}
/// Display used to show the loading animation.
/// {@endtemplate}
class LoadingDisplay extends TextComponent with HasGameRef<PinballGame> {
class LoadingDisplay extends TextComponent {
/// {@template loading_display}
LoadingDisplay();
@ -27,7 +28,7 @@ class LoadingDisplay extends TextComponent with HasGameRef<PinballGame> {
position = Vector2(0, -10);
anchor = Anchor.center;
text = _label = gameRef.l10n.loading;
text = _label = readProvider<AppLocalizations>().loading;
textRenderer = _bodyTextPaint;
await add(

@ -39,14 +39,14 @@ class _BottomGroupSide extends Component {
@override
Future<void> onLoad() async {
final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? 0 : -6.66;
final centerXAdjustment = _side.isLeft ? -0.45 : -6.8;
final flipper = ControlledFlipper(
side: _side,
)..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6);
)..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side)
..initialPosition = Vector2(
(25.58 * direction) + centerXAdjustment,
(25.38 * direction) + centerXAdjustment,
28.71,
);
final kicker = Kicker(
@ -56,7 +56,7 @@ class _BottomGroupSide extends Component {
..applyTo(['bouncy_edge']),
],
)..initialPosition = Vector2(
(22.64 * direction) + centerXAdjustment,
(22.44 * direction) + centerXAdjustment,
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 'backbox/backbox.dart';
export 'bottom_group.dart';
export 'camera_controller.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart';
export 'drain.dart';
export 'drain/drain.dart';
export 'flutter_forest/flutter_forest.dart';
export 'game_bloc_status_listener.dart';
export 'google_word/google_word.dart';

@ -1,6 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters
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';
@ -22,9 +23,7 @@ class ControlledBall extends Ball with Controls<BallController> {
zIndex = ZIndexes.ballOnLaunchRamp;
}
/// {@template bonus_ball}
/// {@macro controlled_ball}
/// {@endtemplate}
ControlledBall.bonus({
required CharacterTheme characterTheme,
}) : 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.
/// {@endtemplate}
class BallController extends ComponentController<Ball>
with HasGameRef<PinballGame> {
with FlameBlocReader<GameBloc, GameState> {
/// {@macro ball_controller}
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
/// sequence runs, then boosts the ball out of the computer.
Future<void> turboCharge() async {
gameRef.read<GameBloc>().add(const SparkyTurboChargeActivated());
bloc.add(const SparkyTurboChargeActivated());
component.stop();
// 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)),
);
}
@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.
/// {@endtemplate}
class FlipperController extends ComponentController<Flipper>
with KeyboardHandler, BlocComponent<GameBloc, GameState> {
with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro flipper_controller}
FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys,
@ -37,7 +37,7 @@ class FlipperController extends ComponentController<Flipper>
RawKeyEvent event,
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 (event is RawKeyDownEvent) {

@ -2,6 +2,7 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -14,13 +15,36 @@ class ControlledPlunger extends Plunger with Controls<PlungerController> {
: super(compressionDistance: compressionDistance) {
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}
/// A [ComponentController] that controls a [Plunger]s movement.
/// {@endtemplate}
class PlungerController extends ComponentController<Plunger>
with KeyboardHandler, BlocComponent<GameBloc, GameState> {
with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro plunger_controller}
PlungerController(Plunger plunger) : super(plunger);
@ -38,7 +62,7 @@ class PlungerController extends ComponentController<Plunger>
RawKeyEvent event,
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 (event is RawKeyDownEvent) {

@ -1,11 +1,12 @@
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';
/// Adds a [GameBonus.dinoChomp] when a [Ball] is chomped by the [ChromeDino].
class ChromeDinoBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<DinoDesert> {
with ParentIsA<DinoDesert>, FlameBlocReader<GameBloc, GameState> {
@override
void onMount() {
super.onMount();
@ -18,7 +19,7 @@ class ChromeDinoBonusBehavior extends Component
final listenWhen = state.status == ChromeDinoStatus.chomping;
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)
..applyTo(['inside_mouth']),
],
)..initialPosition = Vector2(12.6, -6.9),
)..initialPosition = Vector2(12.2, -6.9),
_BarrierBehindDino(),
DinoWalls(),
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_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';
/// 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]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component
with ParentIsA<FlutterForest>, HasGameRef<PinballGame> {
with
ParentIsA<FlutterForest>,
HasGameRef,
FlameBlocReader<GameBloc, GameState> {
@override
void onMount() {
super.onMount();
@ -35,12 +40,11 @@ class FlutterForestBonusBehavior extends Component
}
if (signpost.bloc.isFullyProgressed()) {
gameRef
.read<GameBloc>()
.add(const BonusActivated(GameBonus.dashNest));
bloc.add(const BonusActivated(GameBonus.dashNest));
canvas.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme)
..initialPosition = Vector2(29.5, -24.5),
ControlledBall.bonus(
characterTheme: readProvider<CharacterTheme>(),
)..initialPosition = Vector2(29.2, -24.5),
);
animatronic.playing = true;
signpost.bloc.onProgressed();

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

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

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

@ -14,8 +14,8 @@ class Launcher extends Component {
LaunchRamp(),
Flapper(),
ControlledPlunger(compressionDistance: 9.2)
..initialPosition = Vector2(41.2, 43.7),
RocketSpriteComponent()..position = Vector2(43, 62.3),
..initialPosition = Vector2(41, 43.7),
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.
class MultiballsBehavior extends Component
with
HasGameRef<PinballGame>,
ParentIsA<Multiballs>,
BlocComponent<GameBloc, GameState> {
with ParentIsA<Multiballs>, FlameBlocListenable<GameBloc, GameState> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
final hasChanged = previousState?.bonusHistory != newState.bonusHistory;

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

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

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

@ -17,19 +17,21 @@ import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends PinballForge2DGame
with
FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController>,
MultiTouchTapDetector {
with HasKeyboardHandlerComponents, MultiTouchTapDetector {
PinballGame({
required this.characterTheme,
required CharacterTheme characterTheme,
required this.leaderboardRepository,
required this.l10n,
required this.player,
}) : super(gravity: Vector2(0, 30)) {
required GameBloc gameBloc,
required AppLocalizations l10n,
required PinballPlayer player,
}) : _gameBloc = gameBloc,
_player = player,
_characterTheme = characterTheme,
_l10n = l10n,
super(
gravity: Vector2(0, 30),
) {
images.prefix = '';
controller = _GameBallsController(this);
}
/// Identifier of the play button overlay
@ -38,63 +40,68 @@ class PinballGame extends PinballForge2DGame
@override
Color backgroundColor() => Colors.transparent;
final CharacterTheme characterTheme;
final CharacterTheme _characterTheme;
final PinballPlayer player;
final PinballPlayer _player;
final LeaderboardRepository leaderboardRepository;
final AppLocalizations l10n;
final AppLocalizations _l10n;
final GameBloc _gameBloc;
@override
Future<void> onLoad() async {
await add(CameraController(this));
final machine = [
BoardBackgroundSpriteComponent(),
Boundaries(),
Backbox(leaderboardRepository: leaderboardRepository),
];
final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(),
Multiballs(),
SkillShot(
await add(
FlameBlocProvider<GameBloc, GameState>.value(
value: _gameBloc,
children: [
ScoringContactBehavior(points: Points.oneMillion),
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(),
Boundaries(),
Backbox(leaderboardRepository: leaderboardRepository),
GoogleWord(position: Vector2(-4.45, 1.8)),
Multipliers(),
Multiballs(),
SkillShot(
children: [
ScoringContactBehavior(points: Points.oneMillion),
],
),
AndroidAcres(),
DinoDesert(),
FlutterForest(),
SparkyScorch(),
Drain(),
BottomGroup(),
Launcher(),
],
),
],
),
],
),
],
),
];
final characterAreas = [
AndroidAcres(),
DinoDesert(),
FlutterForest(),
SparkyScorch(),
];
await addAll(
[
GameBlocStatusListener(),
CanvasComponent(
onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [
ZCanvasComponent(
children: [
...machine,
...decals,
...characterAreas,
Drain(),
BottomGroup(),
Launcher(),
],
),
],
),
],
);
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 {
DebugPinballGame({
required CharacterTheme characterTheme,
required LeaderboardRepository leaderboardRepository,
required AppLocalizations l10n,
required PinballPlayer player,
required GameBloc gameBloc,
}) : super(
characterTheme: characterTheme,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: l10n,
) {
controller = _GameBallsController(this);
}
gameBloc: gameBloc,
);
Vector2? lineStart;
Vector2? lineEnd;

@ -40,33 +40,40 @@ class PinballGamePage extends StatelessWidget {
final player = context.read<PinballPlayer>();
final leaderboardRepository = context.read<LeaderboardRepository>();
final game = isDebugMode
? DebugPinballGame(
characterTheme: characterTheme,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
)
: PinballGame(
characterTheme: characterTheme,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
);
return BlocProvider(
create: (_) => GameBloc(),
child: Builder(
builder: (context) {
final gameBloc = context.read<GameBloc>();
final game = isDebugMode
? DebugPinballGame(
characterTheme: characterTheme,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
gameBloc: gameBloc,
)
: PinballGame(
characterTheme: characterTheme,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
gameBloc: gameBloc,
);
final loadables = [
...game.preLoadAssets(),
...player.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
];
final loadables = [
...game.preLoadAssets(),
...player.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
];
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => GameBloc()),
BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()),
],
child: PinballGameView(game: game),
return BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
child: PinballGameView(game: game),
);
},
),
);
}
}
@ -107,14 +114,6 @@ class PinballGameLoadedView extends StatelessWidget {
@override
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(
child: Stack(
children: [
@ -134,16 +133,36 @@ class PinballGameLoadedView extends StatelessWidget {
},
),
),
Positioned(
top: 0,
left: clampedMargin,
child: Visibility(
visible: isPlaying,
child: const GameHud(),
),
),
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,
left: clampedMargin,
child: Visibility(
visible: isPlaying && !isGameOver,
child: const GameHud(),
),
);
}
}

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

@ -14,6 +14,7 @@ class $AssetsImagesGen {
const $AssetsImagesBonusAnimationGen();
$AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
}
@ -53,6 +54,14 @@ class $AssetsImagesComponentsGen {
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 {
const $AssetsImagesScoreGen();

@ -120,6 +120,46 @@
"@footerGoogleIOText": {
"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 its 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": {
"description": "Text shown to indicate loading times"

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

@ -14,10 +14,13 @@ class $AssetsMusicGen {
class $AssetsSfxGen {
const $AssetsSfxGen();
String get afterLaunch => 'assets/sfx/after_launch.mp3';
String get bumperA => 'assets/sfx/bumper_a.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 ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';
String get launcher => 'assets/sfx/launcher.mp3';
}
class Assets {

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

@ -146,6 +146,15 @@ void main() {
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
),
).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(
() => preCacheSingleAudio
.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', () {
test('plays the correct file', () async {
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', () {
test('plays the correct file', () async {
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()
: super(
anchor: Anchor.center,
position: Vector2(0, -1),
position: Vector2(-0.2, 0.1),
) {
zIndex = ZIndexes.boardBackground;
}

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

@ -37,46 +37,46 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
List<FixtureDef> _createFixtureDefs() {
final topEdgeShape = EdgeShape()
..set(
Vector2(29.25, -35.27),
Vector2(28.4, -34.77),
Vector2(29.05, -35.27),
Vector2(28.2, -34.77),
);
final topCurveShape = BezierCurveShape(
controlPoints: [
topEdgeShape.vertex2,
Vector2(21.35, -28.72),
Vector2(23.45, -24.62),
Vector2(21.15, -28.72),
Vector2(23.25, -24.62),
],
);
final tunnelTopEdgeShape = EdgeShape()
..set(
topCurveShape.vertices.last,
Vector2(30.35, -27.32),
Vector2(30.15, -27.32),
);
final tunnelBottomEdgeShape = EdgeShape()
..set(
Vector2(30.75, -23.17),
Vector2(25.45, -21.22),
Vector2(30.55, -23.17),
Vector2(25.25, -21.22),
);
final middleEdgeShape = EdgeShape()
..set(
tunnelBottomEdgeShape.vertex2,
Vector2(27.45, -19.32),
Vector2(27.25, -19.32),
);
final bottomEdgeShape = EdgeShape()
..set(
middleEdgeShape.vertex2,
Vector2(24.65, -15.02),
Vector2(24.45, -15.02),
);
final undersideEdgeShape = EdgeShape()
..set(
bottomEdgeShape.vertex2,
Vector2(31.75, -13.77),
Vector2(31.55, -13.77),
);
return [
@ -108,7 +108,7 @@ class _DinoTopWallSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_DinoTopWallSpriteComponent()
: super(
position: Vector2(22.75, -38.07),
position: Vector2(22.55, -38.07),
) {
zIndex = ZIndexes.dinoTopWall;
}
@ -129,7 +129,7 @@ class _DinoTopWallSpriteComponent extends SpriteComponent
class _DinoTopWallTunnelSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_DinoTopWallTunnelSpriteComponent()
: super(position: Vector2(23.31, -26.01)) {
: super(position: Vector2(23.11, -26.01)) {
zIndex = ZIndexes.dinoTopWallTunnel;
}
@ -162,28 +162,28 @@ class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex {
List<FixtureDef> _createFixtureDefs() {
final topEdgeShape = EdgeShape()
..set(
Vector2(32.4, -8.8),
Vector2(25, -7.7),
Vector2(32.2, -8.8),
Vector2(24.8, -7.7),
);
final topLeftCurveShape = BezierCurveShape(
controlPoints: [
topEdgeShape.vertex2,
Vector2(21.8, -7),
Vector2(29.8, 13.8),
Vector2(21.6, -7),
Vector2(29.6, 13.8),
],
);
final bottomLeftEdgeShape = EdgeShape()
..set(
topLeftCurveShape.vertices.last,
Vector2(31.9, 44.1),
Vector2(31.7, 44.1),
);
final bottomEdgeShape = EdgeShape()
..set(
bottomLeftEdgeShape.vertex2,
Vector2(37.8, 44.1),
Vector2(37.6, 44.1),
);
return [
@ -219,6 +219,6 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef {
);
this.sprite = sprite;
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: [
FlapperSpinningBehavior(),
],
)..initialPosition = Vector2(4, -69.3),
)..initialPosition = Vector2(3.8, -69.3),
_FlapperStructure(),
_FlapperExit()..initialPosition = Vector2(-0.6, -33.8),
_FlapperExit()..initialPosition = Vector2(-0.8, -33.8),
_BackSupportSpriteComponent(),
_FrontSupportSpriteComponent(),
FlapSpriteAnimationComponent(),
@ -73,14 +73,14 @@ class _FlapperStructure extends BodyComponent with Layered {
List<FixtureDef> _createFixtureDefs() {
final leftEdgeShape = EdgeShape()
..set(
Vector2(1.9, -69.3),
Vector2(1.9, -66),
Vector2(1.7, -69.3),
Vector2(1.7, -66),
);
final bottomEdgeShape = EdgeShape()
..set(
leftEdgeShape.vertex2,
Vector2(3.9, -66),
Vector2(3.7, -66),
);
return [
@ -130,7 +130,7 @@ class FlapSpriteAnimationComponent extends SpriteAnimationComponent
FlapSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(2.8, -70.7),
position: Vector2(2.6, -70.7),
playing: false,
) {
zIndex = ZIndexes.flapper;
@ -173,7 +173,7 @@ class _BackSupportSpriteComponent extends SpriteComponent
_BackSupportSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(2.95, -70.6),
position: Vector2(2.75, -70.6),
) {
zIndex = ZIndexes.flapperBack;
}
@ -196,7 +196,7 @@ class _FrontSupportSpriteComponent extends SpriteComponent
_FrontSupportSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(2.9, -67.6),
position: Vector2(2.7, -67.7),
) {
zIndex = ZIndexes.flapperFront;
}

@ -40,22 +40,22 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
final rightStraightShape = EdgeShape()
..set(
Vector2(31.4, -61.4),
Vector2(46.5, 68.4),
Vector2(31, -61.4),
Vector2(46.1, 68.4),
);
final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef);
final leftStraightShape = EdgeShape()
..set(
Vector2(27.8, -61.4),
Vector2(41.5, 68.4),
Vector2(27.4, -61.4),
Vector2(41.1, 68.4),
);
final leftStraightFixtureDef = FixtureDef(leftStraightShape);
fixturesDef.add(leftStraightFixtureDef);
final topCurveShape = ArcShape(
center: Vector2(20.5, -61.1),
center: Vector2(20.1, -61.1),
arcRadius: 11,
angle: 1.6,
rotation: 0.1,
@ -64,7 +64,7 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
fixturesDef.add(topCurveFixtureDef);
final bottomCurveShape = ArcShape(
center: Vector2(19.3, -60.3),
center: Vector2(18.9, -60.3),
arcRadius: 8.5,
angle: 1.48,
rotation: 0.1,
@ -74,16 +74,16 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
final topStraightShape = EdgeShape()
..set(
Vector2(3.7, -70.1),
Vector2(19.1, -72.1),
Vector2(3.3, -70.1),
Vector2(18.7, -72.1),
);
final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef);
final bottomStraightShape = EdgeShape()
..set(
Vector2(3.7, -66.9),
Vector2(19.1, -68.8),
Vector2(3.3, -66.9),
Vector2(18.7, -68.8),
);
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
fixturesDef.add(bottomStraightFixtureDef);
@ -113,7 +113,7 @@ class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {
this.sprite = sprite;
size = sprite.originalSize / 10;
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;
size = sprite.originalSize / 10;
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()
..set(
Vector2(27.6, -57.9),
Vector2(38.1, 42.6),
Vector2(27.2, -57.9),
Vector2(37.7, 42.6),
);
final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef);
final curveShape = ArcShape(
center: Vector2(20.1, -59.3),
center: Vector2(19.7, -59.3),
arcRadius: 7.5,
angle: 1.8,
rotation: -0.13,
@ -166,8 +166,8 @@ class _LaunchRampForegroundRailing extends BodyComponent with ZIndex {
final topStraightShape = EdgeShape()
..set(
Vector2(3.7, -66.8),
Vector2(19.7, -66.8),
Vector2(3.3, -66.8),
Vector2(19.3, -66.8),
);
final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef);
@ -198,6 +198,6 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent
this.sprite = sprite;
size = sprite.originalSize / 10;
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({
Iterable<Component>? children,
}) : this._(
position: Vector2(-23, 7.5),
rotation: -24 * math.pi / 180,
position: Vector2(-23.3, 7.5),
rotation: -27 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
@ -46,8 +46,8 @@ class Multiball extends Component {
Multiball.b({
Iterable<Component>? children,
}) : this._(
position: Vector2(-7.2, -6.2),
rotation: -5 * math.pi / 180,
position: Vector2(-7.65, -6.2),
rotation: -2 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
@ -56,8 +56,8 @@ class Multiball extends Component {
Multiball.c({
Iterable<Component>? children,
}) : this._(
position: Vector2(-0.7, -9.3),
rotation: 2.7 * math.pi / 180,
position: Vector2(-1.1, -9.3),
rotation: 6 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
@ -66,8 +66,8 @@ class Multiball extends Component {
Multiball.d({
Iterable<Component>? children,
}) : this._(
position: Vector2(15, 7),
rotation: 24 * math.pi / 180,
position: Vector2(14.8, 7),
rotation: 27 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);

@ -1,5 +1,6 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -13,16 +14,23 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger}
Plunger({
required this.compressionDistance,
}) : super(renderBody: false) {
}) : super(
renderBody: false,
children: [_PlungerSpriteAnimationGroupComponent()],
) {
zIndex = ZIndexes.plunger;
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.
final double compressionDistance;
late final _PlungerSpriteAnimationGroupComponent _spriteComponent;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
@ -78,8 +86,10 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// Set a constant downward velocity on the [Plunger].
void pull() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
body.linearVelocity = Vector2(0, 7);
_spriteComponent.pull();
sprite.pull();
}
/// 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
/// from its original [initialPosition].
void release() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
_pullingDownTime = 0;
final velocity = (initialPosition.y - body.position.y) * 11;
body.linearVelocity = Vector2(0, velocity);
_spriteComponent.release();
sprite.release();
}
@override
@ -127,9 +139,6 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
_spriteComponent = _PlungerSpriteAnimationGroupComponent();
await add(_spriteComponent);
}
}

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

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

@ -33,7 +33,7 @@ abstract class ZIndexes {
static const outerBoundary = _above + boardBackground;
static const outerBottomBoundary = _above + rocket;
static const outerBottomBoundary = _above + bottomBoundary;
// Bottom Group
@ -77,7 +77,7 @@ abstract class ZIndexes {
static const computerTop = _above + ballOnBoard;
static const computerGlow = _above + ballOnBoard;
static const computerGlow = _above + computerTop;
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
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:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
class _MockMultiplierCubit extends Mock implements MultiplierCubit {}
void main() {
group('Multiplier', () {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName,
@ -25,8 +22,16 @@ void main() {
Assets.images.multiplier.x5.dimmed.keyName,
Assets.images.multiplier.x6.lit.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;
setUp(() {
@ -85,7 +90,7 @@ void main() {
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -116,7 +121,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x2-lit.png'),
);
},
@ -125,7 +130,7 @@ void main() {
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -156,7 +161,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x2-dimmed.png'),
);
},
@ -169,7 +174,7 @@ void main() {
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -200,7 +205,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x3-lit.png'),
);
},
@ -209,7 +214,7 @@ void main() {
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -240,7 +245,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x3-dimmed.png'),
);
},
@ -253,7 +258,7 @@ void main() {
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -284,7 +289,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x4-lit.png'),
);
},
@ -293,7 +298,7 @@ void main() {
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -324,7 +329,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x4-dimmed.png'),
);
},
@ -337,7 +342,7 @@ void main() {
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -368,7 +373,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x5-lit.png'),
);
},
@ -377,7 +382,7 @@ void main() {
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -408,7 +413,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x5-dimmed.png'),
);
},
@ -421,7 +426,7 @@ void main() {
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -452,7 +457,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x6-lit.png'),
);
},
@ -461,7 +466,7 @@ void main() {
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
whenListen(
bloc,
@ -492,7 +497,7 @@ void main() {
);
await expectLater(
find.byGame<TestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/multipliers/x6-dimmed.png'),
);
},

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

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

@ -26,6 +26,7 @@ class ContactBehavior<T extends BodyComponent> extends Component
@override
Future<void> onLoad() async {
await super.onLoad();
if (_fixturesUserData.isNotEmpty) {
for (final fixture in _targetedFixtures) {
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,
);
static const headline5 = TextStyle(
color: PinballColors.white,
fontSize: 14,
package: _fontPackage,
fontFamily: _primaryFontFamily,
);
static const subtitle2 = TextStyle(
color: PinballColors.white,
fontSize: 16,
@ -45,7 +52,7 @@ abstract class PinballTextStyle {
);
static const subtitle1 = TextStyle(
fontSize: 10,
fontSize: 12,
fontFamily: _primaryFontFamily,
package: _fontPackage,
color: PinballColors.yellow,

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

@ -26,9 +26,9 @@ void main() {
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;
expect(style.fontSize, 10);
expect(style.fontSize, 12);
expect(style.color, PinballColors.yellow);
});

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

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

@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@ -7,6 +8,21 @@ import 'package:plugin_platform_interface/plugin_platform_interface.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
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {}
@ -49,7 +65,7 @@ void main() {
).thenAnswer((_) async => true);
await tester.pumpApp(const Footer());
final flutterTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && tapTextSpan(widget, 'Flutter'),
(widget) => widget is RichText && _tapTextSpan(widget, 'Flutter'),
);
await tester.tap(flutterTextFinder);
await tester.pumpAndSettle();
@ -84,7 +100,7 @@ void main() {
).thenAnswer((_) async => true);
await tester.pumpApp(const Footer());
final firebaseTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && tapTextSpan(widget, 'Firebase'),
(widget) => widget is RichText && _tapTextSpan(widget, 'Firebase'),
);
await tester.tap(firebaseTextFinder);
await tester.pumpAndSettle();

@ -6,14 +6,24 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball_audio/pinball_audio.dart';
import '../../helpers/helpers.dart';
import 'package:pinball_flame/pinball_flame.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 {
@override
Body createBody() {
return world.createBody(BodyDef());
}
Body createBody() => world.createBody(BodyDef());
}
class _MockPinballPlayer extends Mock implements PinballPlayer {}
@ -23,12 +33,10 @@ class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('BumperNoisyBehavior', () {});
group('BumperNoiseBehavior', () {});
late PinballPlayer player;
final flameTester = FlameTester(
() => EmptyPinballTestGame(player: player),
);
final flameTester = FlameTester(_TestGame.new);
setUp(() {
player = _MockPinballPlayer();
@ -37,9 +45,9 @@ void main() {
flameTester.testGameWidget(
'plays bumper sound',
setUp: (game, _) async {
final behavior = BumperNoisyBehavior();
final behavior = BumperNoiseBehavior();
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
await game.pump(parent, player: player);
await parent.ensureAdd(behavior);
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
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: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_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 {
@override
@ -27,18 +49,13 @@ class _MockContact extends Mock implements Contact {}
void main() {
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 Ball ball;
late BodyComponent parent;
setUp(() {
bloc = _MockGameBloc();
ball = _MockBall();
final ballBody = _MockBody();
when(() => ball.body).thenReturn(ballBody);
@ -47,23 +64,7 @@ void main() {
parent = _TestBodyComponent();
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
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,
);
final flameBlocTester = FlameTester(_TestGame.new);
group('ScoringBehavior', () {
test('can be instantiated', () {
@ -76,16 +77,16 @@ void main() {
);
});
flameBlocTester.testGameWidget(
flameBlocTester.test(
'can be loaded',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
(game) async {
await game.pump(parent);
final behavior = ScoringBehavior(
points: Points.fiveThousand,
position: Vector2.zero(),
);
await parent.add(behavior);
await game.ensureAdd(canvas);
await parent.ensureAdd(behavior);
expect(
parent.firstChild<ScoringBehavior>(),
@ -94,13 +95,12 @@ void main() {
},
);
flameBlocTester.testGameWidget(
flameBlocTester.test(
'emits Scored event with points when added',
setUp: (game, tester) async {
const points = Points.oneMillion;
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
(game) async {
await game.pump(parent, gameBloc: bloc);
const points = Points.oneMillion;
final behavior = ScoringBehavior(
points: points,
position: Vector2(0, 0),
@ -115,11 +115,10 @@ void main() {
},
);
flameBlocTester.testGameWidget(
flameBlocTester.test(
'correctly renders text',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
(game) async {
await game.pump(parent);
const points = Points.oneMillion;
final position = Vector2.all(1);
@ -145,8 +144,8 @@ void main() {
flameBlocTester.testGameWidget(
'is removed after duration',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
await game.onLoad();
await game.pump(parent);
const duration = 2.0;
final behavior = ScoringBehavior(
@ -173,8 +172,8 @@ void main() {
flameBlocTester.testGameWidget(
'beginContact adds a ScoringBehavior',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
await game.onLoad();
await game.pump(parent);
final behavior = ScoringContactBehavior(points: Points.oneMillion);
await parent.ensureAdd(behavior);
@ -192,8 +191,8 @@ void main() {
flameBlocTester.testGameWidget(
"beginContact positions text at contact's position",
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
await game.onLoad();
await game.pump(parent);
final behavior = ScoringContactBehavior(points: Points.oneMillion);
await parent.ensureAdd(behavior);

@ -1,56 +1,70 @@
// 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: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/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName,
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.android.bumper.a.lit.keyName,
Assets.images.android.bumper.a.dimmed.keyName,
Assets.images.android.bumper.b.lit.keyName,
Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName,
]);
}
Future<void> pump(AndroidAcres child) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [child],
),
);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName,
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.android.bumper.a.lit.keyName,
Assets.images.android.bumper.a.dimmed.keyName,
Assets.images.android.bumper.b.lit.keyName,
Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName,
];
group('AndroidAcres', () {
final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
final flameTester = FlameTester(_TestGame.new);
flameTester.test('loads correctly', (game) async {
final component = AndroidAcres();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
await game.pump(component);
expect(game.descendants(), contains(component));
});
group('loads', () {
flameTester.test(
'an AndroidSpaceship',
(game) async {
await game.ensureAdd(AndroidAcres());
await game.pump(AndroidAcres());
expect(
game.descendants().whereType<AndroidSpaceship>().length,
equals(1),
@ -61,7 +75,7 @@ void main() {
flameTester.test(
'an AndroidAnimatronic',
(game) async {
await game.ensureAdd(AndroidAcres());
await game.pump(AndroidAcres());
expect(
game.descendants().whereType<AndroidAnimatronic>().length,
equals(1),
@ -72,7 +86,7 @@ void main() {
flameTester.test(
'a SpaceshipRamp',
(game) async {
await game.ensureAdd(AndroidAcres());
await game.pump(AndroidAcres());
expect(
game.descendants().whereType<SpaceshipRamp>().length,
equals(1),
@ -83,7 +97,7 @@ void main() {
flameTester.test(
'a SpaceshipRail',
(game) async {
await game.ensureAdd(AndroidAcres());
await game.pump(AndroidAcres());
expect(
game.descendants().whereType<SpaceshipRail>().length,
equals(1),
@ -94,7 +108,7 @@ void main() {
flameTester.test(
'three AndroidBumper',
(game) async {
await game.ensureAdd(AndroidAcres());
await game.pump(AndroidAcres());
expect(
game.descendants().whereType<AndroidBumper>().length,
equals(3),
@ -103,13 +117,13 @@ void main() {
);
flameTester.test(
'three AndroidBumpers with BumperNoisyBehavior',
'three AndroidBumpers with BumperNoiseBehavior',
(game) async {
await game.ensureAdd(AndroidAcres());
await game.pump(AndroidAcres());
final bumpers = game.descendants().whereType<AndroidBumper>();
for (final bumper in bumpers) {
expect(
bumper.firstChild<BumperNoisyBehavior>(),
bumper.firstChild<BumperNoiseBehavior>(),
isNotNull,
);
}
@ -119,7 +133,7 @@ void main() {
flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async {
final androidAcres = AndroidAcres();
await game.ensureAdd(androidAcres);
await game.pump(androidAcres);
expect(
androidAcres.children.whereType<AndroidSpaceshipBonusBehavior>().single,
isNotNull,

@ -1,7 +1,7 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/extensions.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';
@ -9,55 +9,66 @@ import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName,
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.android.bumper.a.lit.keyName,
Assets.images.android.bumper.a.dimmed.keyName,
Assets.images.android.bumper.b.lit.keyName,
Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName,
]);
}
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();
final assets = [
Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName,
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.android.bumper.a.lit.keyName,
Assets.images.android.bumper.a.dimmed.keyName,
Assets.images.android.bumper.b.lit.keyName,
Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName,
];
group('AndroidSpaceshipBonusBehavior', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
final flameTester = FlameTester(_TestGame.new);
flameBlocTester.testGameWidget(
flameTester.testGameWidget(
'adds GameBonus.androidSpaceship to the game '
'when android spacehship has a bonus',
setUp: (game, tester) async {
@ -66,7 +77,10 @@ void main() {
final androidSpaceship = AndroidSpaceship(position: Vector2.zero());
await parent.add(androidSpaceship);
await game.ensureAdd(parent);
await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
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 '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:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@ -12,7 +14,41 @@ import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.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.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.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 {}
@ -23,21 +59,6 @@ class _MockStreamSubscription extends Mock
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.score.oneMillion.keyName,
];
group('RampBonusBehavior', () {
const shotPoints = Points.oneMillion;
@ -46,22 +67,13 @@ void main() {
setUp(() {
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
final flameTester = FlameTester(_TestGame.new);
flameBlocTester.testGameWidget(
flameTester.test(
'when hits are multiples of 10 times adds a ScoringBehavior',
setUp: (game, tester) async {
(game) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
@ -69,14 +81,13 @@ void main() {
streamController.stream,
initialState: SpaceshipRampState(hits: 9),
);
final behavior = RampBonusBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
final behavior = RampBonusBehavior(points: shotPoints);
final parent = SpaceshipRamp.test(bloc: bloc);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
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",
setUp: (game, tester) async {
(game) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
@ -98,14 +109,13 @@ void main() {
streamController.stream,
initialState: SpaceshipRampState.initial(),
);
final behavior = RampBonusBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
final behavior = RampBonusBehavior(points: shotPoints);
final parent = SpaceshipRamp.test(bloc: bloc);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
@ -117,9 +127,9 @@ void main() {
},
);
flameBlocTester.testGameWidget(
flameTester.test(
'closes subscription when removed',
setUp: (game, tester) async {
(game) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
@ -135,11 +145,12 @@ void main() {
points: shotPoints,
subscription: subscription,
);
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);
parent.remove(behavior);

@ -3,6 +3,8 @@
import 'dart:async';
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:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@ -12,7 +14,41 @@ import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.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.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.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 {}
@ -23,21 +59,6 @@ class _MockStreamSubscription extends Mock
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.score.fiveThousand.keyName,
];
group('RampShotBehavior', () {
const shotPoints = Points.fiveThousand;
@ -46,23 +67,14 @@ void main() {
setUp(() {
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
final flameBlocTester = FlameTester(_TestGame.new);
flameBlocTester.testGameWidget(
flameBlocTester.test(
'when hits are not multiple of 10 times '
'increases multiplier and adds a ScoringBehavior',
setUp: (game, tester) async {
(game) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
@ -70,14 +82,13 @@ void main() {
streamController.stream,
initialState: SpaceshipRampState.initial(),
);
final behavior = RampShotBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
final behavior = RampShotBehavior(points: shotPoints);
final parent = SpaceshipRamp.test(bloc: bloc);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
@ -90,10 +101,10 @@ void main() {
},
);
flameBlocTester.testGameWidget(
flameBlocTester.test(
'when hits multiple of 10 times '
"doesn't increase multiplier, neither ScoringBehavior",
setUp: (game, tester) async {
(game) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
@ -108,7 +119,10 @@ void main() {
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10));
@ -121,9 +135,9 @@ void main() {
},
);
flameBlocTester.testGameWidget(
flameBlocTester.test(
'closes subscription when removed',
setUp: (game, tester) async {
(game) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
@ -143,7 +157,10 @@ void main() {
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
parent.remove(behavior);

@ -3,7 +3,9 @@
import 'dart:async';
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:flutter/material.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/l10n/l10n.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 '../../../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 {
@override
@ -44,6 +76,9 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get name => '';
@override
String get rank => '';
@override
String get enterInitials => '';
@ -65,18 +100,8 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const character = theme.AndroidTheme();
final assets = [
character.leaderboardIcon.keyName,
Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(
assets: assets,
l10n: _MockAppLocalizations(),
),
);
final flameTester = FlameTester(_TestGame.new);
late BackboxBloc bloc;
@ -84,7 +109,7 @@ void main() {
bloc = _MockBackboxBloc();
whenListen(
bloc,
Stream.value(LoadingState()),
Stream<BackboxState>.empty(),
initialState: LoadingState(),
);
});
@ -94,27 +119,36 @@ void main() {
'loads correctly',
(game) async {
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(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
game.camera
..followVector2(Vector2(0, -130))
..zoom = 6;
await game.ensureAdd(
await game.pump(
Backbox.test(bloc: bloc),
);
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<EmptyPinballTestGame>(),
find.byGame<_TestGame>(),
matchesGoldenFile('../golden/backbox.png'),
);
},
@ -128,10 +162,10 @@ void main() {
leaderboardRepository: _MockLeaderboardRepository(),
),
);
await game.ensureAdd(backbox);
await game.pump(backbox);
backbox.requestInitials(
score: 0,
character: character,
character: game.character,
);
await game.ready();
@ -148,15 +182,15 @@ void main() {
final bloc = _MockBackboxBloc();
final state = InitialsFormState(
score: 10,
character: theme.AndroidTheme(),
character: game.character,
);
whenListen(
bloc,
Stream.value(state),
Stream<BackboxState>.empty(),
initialState: state,
);
final backbox = Backbox.test(bloc: bloc);
await game.ensureAdd(backbox);
await game.pump(backbox);
game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {});
verify(
@ -164,7 +198,7 @@ void main() {
PlayerInitialsSubmitted(
score: 10,
initials: 'AAA',
character: theme.AndroidTheme(),
character: game.character,
),
),
).called(1);
@ -176,11 +210,11 @@ void main() {
(game) async {
whenListen(
bloc,
Stream.value(InitialsSuccessState()),
Stream<BackboxState>.empty(),
initialState: InitialsSuccessState(),
);
final backbox = Backbox.test(bloc: bloc);
await game.ensureAdd(backbox);
await game.pump(backbox);
expect(
game
@ -197,11 +231,11 @@ void main() {
(game) async {
whenListen(
bloc,
Stream.value(InitialsFailureState()),
Stream<BackboxState>.empty(),
initialState: InitialsFailureState(),
);
final backbox = Backbox.test(bloc: bloc);
await game.ensureAdd(backbox);
await game.pump(backbox);
expect(
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(
'closes the subscription when it is removed',
(game) async {
@ -224,7 +277,7 @@ void main() {
);
final backbox = Backbox.test(bloc: bloc);
await game.ensureAdd(backbox);
await game.pump(backbox);
backbox.removeFromParent();
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