Merge branch 'main' into feat/firestore-rules

pull/322/head
Tom Arra 3 years ago committed by GitHub
commit b3bc2fb35d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,3 +13,4 @@ jobs:
flutter_channel: stable flutter_channel: stable
flutter_version: 2.10.0 flutter_version: 2.10.0
coverage_excludes: "lib/gen/*.dart" coverage_excludes: "lib/gen/*.dart"
test_optimization: false

@ -5,6 +5,29 @@
"hosting": { "hosting": {
"public": "build/web", "public": "build/web",
"site": "ashehwkdkdjruejdnensjsjdne", "site": "ashehwkdkdjruejdnensjsjdne",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"] "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"headers": [
{
"source": "**/*.@(jpg|jpeg|gif|png)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=3600"
}
]
},
{
"source": "**",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache, no-store, must-revalidate"
}
]
}
]
},
"storage": {
"rules": "storage.rules"
} }
} }

@ -8,6 +8,7 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -34,8 +35,11 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _pinballAudio), RepositoryProvider.value(value: _pinballAudio),
], ],
child: BlocProvider( child: MultiBlocProvider(
create: (context) => CharacterThemeCubit(), providers: [
BlocProvider(create: (_) => CharacterThemeCubit()),
BlocProvider(create: (_) => StartGameBloc()),
],
child: MaterialApp( child: MaterialApp(
title: 'I/O Pinball', title: 'I/O Pinball',
theme: PinballTheme.standard, theme: PinballTheme.standard,

@ -0,0 +1,2 @@
export 'bumper_noisy_behavior.dart';
export 'scoring_behavior.dart';

@ -0,0 +1,14 @@
// 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_flame/pinball_flame.dart';
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
gameRef.audio.bumper();
}
}

@ -0,0 +1,77 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template scoring_behavior}
/// Adds [_points] to the score and shows a text effect.
///
/// The behavior removes itself after the duration.
/// {@endtemplate}
class ScoringBehavior extends Component with HasGameRef<PinballGame> {
/// {@macto scoring_behavior}
ScoringBehavior({
required Points points,
required Vector2 position,
double duration = 1,
}) : _points = points,
_position = position,
_effectController = EffectController(
duration: duration,
);
final Points _points;
final Vector2 _position;
final EffectController _effectController;
@override
void update(double dt) {
super.update(dt);
if (_effectController.completed) {
removeFromParent();
}
}
@override
Future<void> onLoad() async {
gameRef.read<GameBloc>().add(Scored(points: _points.value));
await gameRef.firstChild<ZCanvasComponent>()!.add(
ScoreComponent(
points: _points,
position: _position,
effectController: _effectController,
),
);
}
}
/// {@template scoring_contact_behavior}
/// Adds points to the score when the [Ball] contacts the [parent].
/// {@endtemplate}
class ScoringContactBehavior extends ContactBehavior
with HasGameRef<PinballGame> {
/// {@macro scoring_contact_behavior}
ScoringContactBehavior({
required Points points,
}) : _points = points;
final Points _points;
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.add(
ScoringBehavior(
points: _points,
position: other.body.position,
),
);
}
}

@ -17,12 +17,13 @@ class GameBloc extends Bloc<GameEvent, GameState> {
} }
void _onRoundLost(RoundLost event, Emitter emit) { void _onRoundLost(RoundLost event, Emitter emit) {
final score = state.score * state.multiplier; final score = state.totalScore + state.roundScore * state.multiplier;
final roundsLeft = math.max(state.rounds - 1, 0); final roundsLeft = math.max(state.rounds - 1, 0);
emit( emit(
state.copyWith( state.copyWith(
score: score, totalScore: score,
roundScore: 0,
multiplier: 1, multiplier: 1,
rounds: roundsLeft, rounds: roundsLeft,
), ),
@ -32,7 +33,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
void _onScored(Scored event, Emitter emit) { void _onScored(Scored event, Emitter emit) {
if (!state.isGameOver) { if (!state.isGameOver) {
emit( emit(
state.copyWith(score: state.score + event.points), state.copyWith(roundScore: state.roundScore + event.points),
); );
} }
} }

@ -26,22 +26,32 @@ enum GameBonus {
class GameState extends Equatable { class GameState extends Equatable {
/// {@macro game_state} /// {@macro game_state}
const GameState({ const GameState({
required this.score, required this.totalScore,
required this.roundScore,
required this.multiplier, required this.multiplier,
required this.rounds, required this.rounds,
required this.bonusHistory, required this.bonusHistory,
}) : assert(score >= 0, "Score can't be negative"), }) : assert(totalScore >= 0, "TotalScore can't be negative"),
assert(roundScore >= 0, "Round score can't be negative"),
assert(multiplier > 0, 'Multiplier must be greater than zero'), assert(multiplier > 0, 'Multiplier must be greater than zero'),
assert(rounds >= 0, "Number of rounds can't be negative"); assert(rounds >= 0, "Number of rounds can't be negative");
const GameState.initial() const GameState.initial()
: score = 0, : totalScore = 0,
roundScore = 0,
multiplier = 1, multiplier = 1,
rounds = 3, rounds = 3,
bonusHistory = const []; bonusHistory = const [];
/// The current score of the game. /// The score for the current round of the game.
final int score; ///
/// Multipliers are only applied to the score for the current round once is
/// lost. Then the [roundScore] is added to the [totalScore] and reset to 0
/// for the next round.
final int roundScore;
/// The total score of the game.
final int totalScore;
/// The current multiplier for the score. /// The current multiplier for the score.
final int multiplier; final int multiplier;
@ -58,20 +68,25 @@ class GameState extends Equatable {
/// Determines when the game is over. /// Determines when the game is over.
bool get isGameOver => rounds == 0; bool get isGameOver => rounds == 0;
/// The score displayed at the game.
int get displayScore => roundScore + totalScore;
GameState copyWith({ GameState copyWith({
int? score, int? totalScore,
int? roundScore,
int? multiplier, int? multiplier,
int? balls, int? balls,
int? rounds, int? rounds,
List<GameBonus>? bonusHistory, List<GameBonus>? bonusHistory,
}) { }) {
assert( assert(
score == null || score >= this.score, totalScore == null || totalScore >= this.totalScore,
"Score can't be decreased", "Total score can't be decreased",
); );
return GameState( return GameState(
score: score ?? this.score, totalScore: totalScore ?? this.totalScore,
roundScore: roundScore ?? this.roundScore,
multiplier: multiplier ?? this.multiplier, multiplier: multiplier ?? this.multiplier,
rounds: rounds ?? this.rounds, rounds: rounds ?? this.rounds,
bonusHistory: bonusHistory ?? this.bonusHistory, bonusHistory: bonusHistory ?? this.bonusHistory,
@ -80,7 +95,8 @@ class GameState extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
score, totalScore,
roundScore,
multiplier, multiplier,
rounds, rounds,
bonusHistory, bonusHistory,

@ -2,8 +2,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template android_acres} /// {@template android_acres}
@ -15,27 +15,39 @@ class AndroidAcres extends Component {
AndroidAcres() AndroidAcres()
: super( : super(
children: [ children: [
SpaceshipRamp(), SpaceshipRamp(
children: [
RampShotBehavior(
points: Points.fiveThousand,
),
RampBonusBehavior(
points: Points.oneMillion,
),
],
),
SpaceshipRail(), SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic( AndroidAnimatronic(
children: [ children: [
ScoringBehavior(points: Points.twoHundredThousand), ScoringContactBehavior(points: Points.twoHundredThousand),
], ],
)..initialPosition = Vector2(-26, -28.25), )..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a( AndroidBumper.a(
children: [ children: [
BumperScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-25, 1.3), )..initialPosition = Vector2(-25, 1.3),
AndroidBumper.b( AndroidBumper.b(
children: [ children: [
BumperScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-32.8, -9.2), )..initialPosition = Vector2(-32.8, -9.2),
AndroidBumper.cow( AndroidBumper.cow(
children: [ children: [
BumperScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-20.5, -13.8), )..initialPosition = Vector2(-20.5, -13.8),
AndroidSpaceshipBonusBehavior(), AndroidSpaceshipBonusBehavior(),

@ -1 +1,3 @@
export 'android_spaceship_bonus_behavior.dart'; export 'android_spaceship_bonus_behavior.dart';
export 'ramp_bonus_behavior.dart';
export 'ramp_shot_behavior.dart';

@ -0,0 +1,62 @@
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> {
/// {@macro ramp_bonus_behavior}
RampBonusBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampBonusBehavior].
///
/// This can be used for testing [RampBonusBehavior] in isolation.
@visibleForTesting
RampBonusBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (achievedOneMillionPoints) {
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -60),
duration: 2,
),
);
}
});
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
}
}

@ -0,0 +1,63 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/cupertino.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_shot_behavior}
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
/// {@endtemplate}
class RampShotBehavior extends Component
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
/// {@macro ramp_shot_behavior}
RampShotBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampShotBehavior].
///
/// This can be used for testing [RampShotBehavior] in isolation.
@visibleForTesting
RampShotBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (!achievedOneMillionPoints) {
gameRef.read<GameBloc>().add(const MultiplierIncreased());
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -45),
),
);
}
});
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
}
}

@ -0,0 +1,56 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template backbox}
/// The [Backbox] of the pinball machine.
/// {@endtemplate}
class Backbox extends PositionComponent with HasGameRef, ZIndex {
/// {@macro backbox}
Backbox()
: super(
position: Vector2(0, -87),
anchor: Anchor.bottomCenter,
children: [
_BackboxSpriteComponent(),
],
) {
zIndex = ZIndexes.backbox;
}
/// Puts [InitialsInputDisplay] on the [Backbox].
Future<void> initialsInput({
required int score,
required String characterIconPath,
InitialsOnSubmit? onSubmit,
}) async {
removeAll(children.where((child) => child is! _BackboxSpriteComponent));
await add(
InitialsInputDisplay(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
),
);
}
}
class _BackboxSpriteComponent extends SpriteComponent with HasGameRef {
_BackboxSpriteComponent() : super(anchor: Anchor.bottomCenter);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.backbox.marquee.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}

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

@ -0,0 +1,387 @@
import 'dart:async';
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_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Signature for the callback called when the used has
/// submitted their initials on the [InitialsInputDisplay].
typedef InitialsOnSubmit = void Function(String);
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 3,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
final _subtitleTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.8,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template initials_input_display}
/// Display that handles the user input on the game over view.
/// {@endtemplate}
// TODO(allisonryan0002): add mobile input buttons.
class InitialsInputDisplay extends Component with HasGameRef {
/// {@macro initials_input_display}
InitialsInputDisplay({
required int score,
required String characterIconPath,
InitialsOnSubmit? onSubmit,
}) : _onSubmit = onSubmit,
super(
children: [
_ScoreLabelTextComponent(),
_ScoreTextComponent(score.formatScore()),
_NameLabelTextComponent(),
_CharacterIconSpriteComponent(characterIconPath),
_DividerSpriteComponent(),
_InstructionsComponent(),
],
);
final InitialsOnSubmit? _onSubmit;
@override
Future<void> onLoad() async {
for (var i = 0; i < 3; i++) {
await add(
InitialsLetterPrompt(
position: Vector2(
11.4 + (2.3 * i),
-20,
),
hasFocus: i == 0,
),
);
}
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowLeft: () => _movePrompt(true),
LogicalKeyboardKey.arrowRight: () => _movePrompt(false),
LogicalKeyboardKey.enter: _submit,
},
),
);
}
/// Returns the current inputed initials
String get initials => children
.whereType<InitialsLetterPrompt>()
.map((prompt) => prompt.char)
.join();
bool _submit() {
_onSubmit?.call(initials);
return true;
}
bool _movePrompt(bool left) {
final prompts = children.whereType<InitialsLetterPrompt>().toList();
final current = prompts.firstWhere((prompt) => prompt.hasFocus)
..hasFocus = false;
var index = prompts.indexOf(current) + (left ? -1 : 1);
index = min(max(0, index), prompts.length - 1);
prompts[index].hasFocus = true;
return false;
}
}
class _ScoreLabelTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_ScoreLabelTextComponent()
: super(
anchor: Anchor.centerLeft,
position: Vector2(-16.9, -24),
textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith(
color: PinballColors.red,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.score;
}
}
class _ScoreTextComponent extends TextComponent {
_ScoreTextComponent(String score)
: super(
text: score,
anchor: Anchor.centerLeft,
position: Vector2(-16.9, -20),
textRenderer: _bodyTextPaint,
);
}
class _NameLabelTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_NameLabelTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(11.4, -24),
textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith(
color: PinballColors.red,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.name;
}
}
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
_CharacterIconSpriteComponent(String characterIconPath)
: _characterIconPath = characterIconPath,
super(
anchor: Anchor.center,
position: Vector2(8.4, -20),
);
final String _characterIconPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
/// {@template initials_input_display}
/// Display that handles the user input on the game over view.
/// {@endtemplate}
@visibleForTesting
class InitialsLetterPrompt extends PositionComponent {
/// {@macro initials_input_display}
InitialsLetterPrompt({
required Vector2 position,
bool hasFocus = false,
}) : _hasFocus = hasFocus,
super(
position: position,
);
static const _alphabetCode = 65;
static const _alphabetLength = 25;
var _charIndex = 0;
bool _hasFocus;
late RectangleComponent _underscore;
late TextComponent _input;
late TimerComponent _underscoreBlinker;
@override
Future<void> onLoad() async {
_underscore = RectangleComponent(
size: Vector2(1.9, 0.4),
anchor: Anchor.center,
position: Vector2(-0.1, 1.8),
);
await add(_underscore);
_input = TextComponent(
text: 'A',
textRenderer: _bodyTextPaint,
anchor: Anchor.center,
);
await add(_input);
_underscoreBlinker = TimerComponent(
period: 0.6,
repeat: true,
autoStart: _hasFocus,
onTick: () {
_underscore.paint.color = (_underscore.paint.color == Colors.white)
? Colors.transparent
: Colors.white;
},
);
await add(_underscoreBlinker);
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowUp: () => _cycle(true),
LogicalKeyboardKey.arrowDown: () => _cycle(false),
},
),
);
}
/// Returns the current selected character
String get char => String.fromCharCode(_alphabetCode + _charIndex);
bool _cycle(bool up) {
if (_hasFocus) {
final newCharCode =
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength);
_input.text = String.fromCharCode(_alphabetCode + newCharCode);
_charIndex = newCharCode;
return false;
}
return true;
}
/// Returns if this prompt has focus on it
bool get hasFocus => _hasFocus;
/// Updates this prompt focus
set hasFocus(bool hasFocus) {
if (hasFocus) {
_underscoreBlinker.timer.resume();
} else {
_underscoreBlinker.timer.pause();
}
_underscore.paint.color = Colors.white;
_hasFocus = hasFocus;
}
}
class _DividerSpriteComponent extends SpriteComponent with HasGameRef {
_DividerSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -17),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(Assets.images.backbox.displayDivider.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
class _InstructionsComponent extends PositionComponent with HasGameRef {
_InstructionsComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -12.3),
children: [
_EnterInitialsTextComponent(),
_ArrowsTextComponent(),
_AndPressTextComponent(),
_EnterReturnTextComponent(),
_ToSubmitTextComponent(),
],
);
}
class _EnterInitialsTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_EnterInitialsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.enterInitials;
}
}
class _ArrowsTextComponent extends TextComponent with HasGameRef<PinballGame> {
_ArrowsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-13.2, 0),
textRenderer: _subtitleTextPaint.copyWith(
(style) => style.copyWith(
fontWeight: FontWeight.bold,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.arrows;
}
}
class _AndPressTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_AndPressTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-3.7, 0),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.andPress;
}
}
class _EnterReturnTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_EnterReturnTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(10, 0),
textRenderer: _subtitleTextPaint.copyWith(
(style) => style.copyWith(
fontWeight: FontWeight.bold,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.enterReturn;
}
}
class _ToSubmitTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_ToSubmitTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.toSubmit;
}
}

@ -1,4 +1,5 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -51,7 +52,8 @@ class _BottomGroupSide extends Component {
final kicker = Kicker( final kicker = Kicker(
side: _side, side: _side,
children: [ children: [
ScoringBehavior(points: Points.fiveThousand)..applyTo(['bouncy_edge']), ScoringContactBehavior(points: Points.fiveThousand)
..applyTo(['bouncy_edge']),
], ],
)..initialPosition = Vector2( )..initialPosition = Vector2(
(22.64 * direction) + centerXAdjustment, (22.64 * direction) + centerXAdjustment,

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

@ -1,4 +1,5 @@
export 'android_acres/android_acres.dart'; export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart';
export 'bottom_group.dart'; export 'bottom_group.dart';
export 'camera_controller.dart'; export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';
@ -12,5 +13,4 @@ export 'google_word/google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'multiballs/multiballs.dart'; export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart'; export 'multipliers/multipliers.dart';
export 'scoring_behavior.dart';
export 'sparky_scorch.dart'; export 'sparky_scorch.dart';

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

@ -1,6 +1,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart'; import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -16,7 +17,7 @@ class DinoDesert extends Component {
children: [ children: [
ChromeDino( ChromeDino(
children: [ children: [
ScoringBehavior(points: Points.twoHundredThousand) ScoringContactBehavior(points: Points.twoHundredThousand)
..applyTo(['inside_mouth']), ..applyTo(['inside_mouth']),
], ],
)..initialPosition = Vector2(12.6, -6.9), )..initialPosition = Vector2(12.6, -6.9),

@ -2,8 +2,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart'; import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -18,22 +18,26 @@ class FlutterForest extends Component with ZIndex {
children: [ children: [
Signpost( Signpost(
children: [ children: [
BumperScoringBehavior(points: Points.fiveThousand), ScoringContactBehavior(points: Points.fiveThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(8.35, -58.3), )..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main( DashNestBumper.main(
children: [ children: [
BumperScoringBehavior(points: Points.twoHundredThousand), ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(18.55, -59.35), )..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a( DashNestBumper.a(
children: [ children: [
BumperScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(8.95, -51.95), )..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b( DashNestBumper.b(
children: [ children: [
BumperScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(22.3, -46.75), )..initialPosition = Vector2(22.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66), DashAnimatronic()..position = Vector2(20, -66),

@ -1,7 +1,6 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template game_flow_controller} /// {@template game_flow_controller}
@ -20,27 +19,26 @@ class GameFlowController extends ComponentController<PinballGame>
@override @override
void onNewState(GameState state) { void onNewState(GameState state) {
if (state.isGameOver) { if (state.isGameOver) {
gameOver(); _initialsInput();
} else { } else {
start(); start();
} }
} }
/// Puts the game on a game over state /// Puts the game in the initials input state.
void gameOver() { void _initialsInput() {
// TODO(erickzanardo): implement score submission and "navigate" to the // TODO(erickzanardo): implement score submission and "navigate" to the
// next page // next page
component.firstChild<Backboard>()?.gameOverMode( component.descendants().whereType<Backbox>().first.initialsInput(
score: state?.score ?? 0, score: state?.displayScore ?? 0,
characterIconPath: component.characterTheme.leaderboardIcon.keyName, characterIconPath: component.characterTheme.leaderboardIcon.keyName,
); );
component.firstChild<CameraController>()?.focusOnBackboard(); component.firstChild<CameraController>()!.focusOnGameOverBackbox();
} }
/// Puts the game on a playing state /// Puts the game in the playing state.
void start() { void start() {
component.audio.backgroundMusic(); component.audio.backgroundMusic();
component.firstChild<Backboard>()?.waitingMode();
component.firstChild<CameraController>()?.focusOnGame(); component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay); component.overlays.remove(PinballGame.playButtonOverlay);
} }

@ -1,7 +1,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/scoring_behavior.dart';
import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; import 'package:pinball/game/components/google_word/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -16,27 +16,27 @@ class GoogleWord extends Component with ZIndex {
children: [ children: [
GoogleLetter( GoogleLetter(
0, 0,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-13.1, 1.72), )..initialPosition = position + Vector2(-13.1, 1.72),
GoogleLetter( GoogleLetter(
1, 1,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-8.33, -0.75), )..initialPosition = position + Vector2(-8.33, -0.75),
GoogleLetter( GoogleLetter(
2, 2,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-2.88, -1.85), )..initialPosition = position + Vector2(-2.88, -1.85),
GoogleLetter( GoogleLetter(
3, 3,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(2.88, -1.85), )..initialPosition = position + Vector2(2.88, -1.85),
GoogleLetter( GoogleLetter(
4, 4,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(8.33, -0.75), )..initialPosition = position + Vector2(8.33, -0.75),
GoogleLetter( GoogleLetter(
5, 5,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(13.1, 1.72), )..initialPosition = position + Vector2(13.1, 1.72),
GoogleWordBonusBehavior(), GoogleWordBonusBehavior(),
], ],

@ -1,53 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template scoring_behavior}
/// Adds points to the score when the ball contacts the [parent].
/// {@endtemplate}
class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> {
/// {@macro scoring_behavior}
ScoringBehavior({
required Points points,
}) : _points = points;
final Points _points;
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
gameRef.read<GameBloc>().add(Scored(points: _points.value));
gameRef.firstChild<ZCanvasComponent>()!.add(
ScoreComponent(
points: _points,
position: other.body.position,
),
);
}
}
/// {@template bumper_scoring_behavior}
/// A specific [ScoringBehavior] used for Bumpers.
/// In addition to its parent logic, also plays the
/// SFX for bumpers
/// {@endtemplate}
class BumperScoringBehavior extends ScoringBehavior {
/// {@macro bumper_scoring_behavior}
BumperScoringBehavior({
required Points points,
}) : super(points: points);
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
gameRef.audio.bumper();
}
}

@ -2,7 +2,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/components.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_scorch} /// {@template sparky_scorch}
@ -16,17 +17,20 @@ class SparkyScorch extends Component {
children: [ children: [
SparkyBumper.a( SparkyBumper.a(
children: [ children: [
BumperScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-22.9, -41.65), )..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b( SparkyBumper.b(
children: [ children: [
BumperScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-21.25, -57.9), )..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c( SparkyBumper.c(
children: [ children: [
BumperScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9), SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9),
@ -47,7 +51,7 @@ class SparkyComputerSensor extends BodyComponent
: super( : super(
renderBody: false, renderBody: false,
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
], ],
); );

@ -99,8 +99,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName), images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName),
images.load(components.Assets.images.sparky.bumper.c.lit.keyName), images.load(components.Assets.images.sparky.bumper.c.lit.keyName),
images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName), images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName),
images.load(components.Assets.images.backboard.backboardScores.keyName), images.load(components.Assets.images.backbox.marquee.keyName),
images.load(components.Assets.images.backboard.backboardGameOver.keyName), images.load(components.Assets.images.backbox.displayDivider.keyName),
images.load(components.Assets.images.googleWord.letter1.lit.keyName), images.load(components.Assets.images.googleWord.letter1.lit.keyName),
images.load(components.Assets.images.googleWord.letter1.dimmed.keyName), images.load(components.Assets.images.googleWord.letter1.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter2.lit.keyName), images.load(components.Assets.images.googleWord.letter2.lit.keyName),
@ -113,7 +113,6 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter5.dimmed.keyName), images.load(components.Assets.images.googleWord.letter5.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter6.lit.keyName), images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName), images.load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiball.lit.keyName), images.load(components.Assets.images.multiball.lit.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName), images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName),

@ -8,6 +8,7 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -22,6 +23,7 @@ class PinballGame extends PinballForge2DGame
PinballGame({ PinballGame({
required this.characterTheme, required this.characterTheme,
required this.audio, required this.audio,
required this.l10n,
}) : super(gravity: Vector2(0, 30)) { }) : super(gravity: Vector2(0, 30)) {
images.prefix = ''; images.prefix = '';
controller = _GameBallsController(this); controller = _GameBallsController(this);
@ -37,6 +39,8 @@ class PinballGame extends PinballForge2DGame
final PinballAudio audio; final PinballAudio audio;
final AppLocalizations l10n;
late final GameFlowController gameFlowController; late final GameFlowController gameFlowController;
@override @override
@ -47,7 +51,7 @@ class PinballGame extends PinballForge2DGame
final machine = [ final machine = [
BoardBackgroundSpriteComponent(), BoardBackgroundSpriteComponent(),
Boundaries(), Boundaries(),
Backboard.waiting(position: Vector2(0, -88)), Backbox(),
]; ];
final decals = [ final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)), GoogleWord(position: Vector2(-4.25, 1.8)),
@ -77,7 +81,7 @@ class PinballGame extends PinballForge2DGame
await super.onLoad(); await super.onLoad();
} }
BoardSide? focusedBoardSide; final focusedBoardSide = <int, BoardSide>{};
@override @override
void onTapDown(int pointerId, TapDownInfo info) { void onTapDown(int pointerId, TapDownInfo info) {
@ -90,9 +94,10 @@ class PinballGame extends PinballForge2DGame
descendants().whereType<Plunger>().single.pullFor(2); descendants().whereType<Plunger>().single.pullFor(2);
} else { } else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; final leftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; focusedBoardSide[pointerId] =
leftSide ? BoardSide.left : BoardSide.right;
final flippers = descendants().whereType<Flipper>().where((flipper) { final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide; return flipper.side == focusedBoardSide[pointerId];
}); });
flippers.first.moveUp(); flippers.first.moveUp();
} }
@ -103,23 +108,23 @@ class PinballGame extends PinballForge2DGame
@override @override
void onTapUp(int pointerId, TapUpInfo info) { void onTapUp(int pointerId, TapUpInfo info) {
_moveFlippersDown(); _moveFlippersDown(pointerId);
super.onTapUp(pointerId, info); super.onTapUp(pointerId, info);
} }
@override @override
void onTapCancel(int pointerId) { void onTapCancel(int pointerId) {
_moveFlippersDown(); _moveFlippersDown(pointerId);
super.onTapCancel(pointerId); super.onTapCancel(pointerId);
} }
void _moveFlippersDown() { void _moveFlippersDown(int pointerId) {
if (focusedBoardSide != null) { if (focusedBoardSide[pointerId] != null) {
final flippers = descendants().whereType<Flipper>().where((flipper) { final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide; return flipper.side == focusedBoardSide[pointerId];
}); });
flippers.first.moveDown(); flippers.first.moveDown();
focusedBoardSide = null; focusedBoardSide.remove(pointerId);
} }
} }
} }
@ -163,20 +168,27 @@ class _GameBallsController extends ComponentController<PinballGame>
} }
} }
class DebugPinballGame extends PinballGame with FPSCounter { class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({ DebugPinballGame({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
required PinballAudio audio, required PinballAudio audio,
required AppLocalizations l10n,
}) : super( }) : super(
characterTheme: characterTheme, characterTheme: characterTheme,
audio: audio, audio: audio,
l10n: l10n,
) { ) {
controller = _GameBallsController(this); controller = _GameBallsController(this);
} }
Vector2? lineStart;
Vector2? lineEnd;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await add(PreviewLine());
await add(_DebugInformation()); await add(_DebugInformation());
} }
@ -190,10 +202,57 @@ class DebugPinballGame extends PinballGame with FPSCounter {
firstChild<ZCanvasComponent>()?.add(ball); firstChild<ZCanvasComponent>()?.add(ball);
} }
} }
@override
void onPanStart(DragStartInfo info) {
lineStart = info.eventPosition.game;
}
@override
void onPanUpdate(DragUpdateInfo info) {
lineEnd = info.eventPosition.game;
}
@override
void onPanEnd(DragEndInfo info) {
if (lineEnd != null) {
final line = lineEnd! - lineStart!;
_turboChargeBall(line);
lineEnd = null;
lineStart = null;
}
}
void _turboChargeBall(Vector2 line) {
final ball = ControlledBall.debug()..initialPosition = lineStart!;
final impulse = line * -1 * 10;
ball.add(BallTurboChargingBehavior(impulse: impulse));
firstChild<ZCanvasComponent>()?.add(ball);
}
} }
// TODO(wolfenrain): investigate this CI failure.
// coverage:ignore-start // coverage:ignore-start
class PreviewLine extends PositionComponent with HasGameRef<DebugPinballGame> {
static final _previewLinePaint = Paint()
..color = Colors.pink
..strokeWidth = 0.4
..style = PaintingStyle.stroke;
@override
void render(Canvas canvas) {
super.render(canvas);
if (gameRef.lineEnd != null) {
canvas.drawLine(
gameRef.lineStart!.toOffset(),
gameRef.lineEnd!.toOffset(),
_previewLinePaint,
);
}
}
}
// TODO(wolfenrain): investigate this CI failure.
class _DebugInformation extends Component with HasGameRef<DebugPinballGame> { class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
@override @override
PositionType get positionType => PositionType.widget; PositionType get positionType => PositionType.widget;

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
@ -39,8 +40,16 @@ class PinballGamePage extends StatelessWidget {
final pinballAudio = context.read<PinballAudio>(); final pinballAudio = context.read<PinballAudio>();
final game = isDebugMode final game = isDebugMode
? DebugPinballGame(characterTheme: characterTheme, audio: audio) ? DebugPinballGame(
: PinballGame(characterTheme: characterTheme, audio: audio); characterTheme: characterTheme,
audio: audio,
l10n: context.l10n,
)
: PinballGame(
characterTheme: characterTheme,
audio: audio,
l10n: context.l10n,
);
final loadables = [ final loadables = [
...game.preLoadAssets(), ...game.preLoadAssets(),
@ -51,7 +60,6 @@ class PinballGamePage extends StatelessWidget {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (_) => StartGameBloc(game: game)),
BlocProvider(create: (_) => GameBloc()), BlocProvider(create: (_) => GameBloc()),
BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()), BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()),
], ],
@ -96,36 +104,43 @@ class PinballGameLoadedView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isPlaying = context.select(
(StartGameBloc bloc) => bloc.state.status == StartGameStatus.play,
);
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
return Stack( return StartGameListener(
children: [ game: game,
Positioned.fill( child: Stack(
child: GameWidget<PinballGame>( children: [
game: game, Positioned.fill(
initialActiveOverlays: const [PinballGame.playButtonOverlay], child: GameWidget<PinballGame>(
overlayBuilderMap: { game: game,
PinballGame.playButtonOverlay: (context, game) { initialActiveOverlays: const [PinballGame.playButtonOverlay],
return Positioned( overlayBuilderMap: {
bottom: 20, PinballGame.playButtonOverlay: (context, game) {
right: 0, return const Positioned(
left: 0, bottom: 20,
child: PlayButtonOverlay(game: game), right: 0,
); left: 0,
child: PlayButtonOverlay(),
);
},
}, },
}, ),
), ),
), Positioned(
// TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc top: 16,
// status left: leftMargin,
Positioned( child: Visibility(
top: 16, visible: isPlaying,
left: leftMargin, child: const GameHud(),
child: const GameHud(), ),
), ),
], ],
),
); );
} }
} }

@ -7,8 +7,8 @@ import 'package:pinball_ui/pinball_ui.dart';
/// {@template game_hud} /// {@template game_hud}
/// Overlay on the [PinballGame]. /// Overlay on the [PinballGame].
/// ///
/// Displays the current [GameState.score], [GameState.rounds] and animates when /// Displays the current [GameState.displayScore], [GameState.rounds] and
/// the player gets a [GameBonus]. /// animates when the player gets a [GameBonus].
/// {@endtemplate} /// {@endtemplate}
class GameHud extends StatefulWidget { class GameHud extends StatefulWidget {
/// {@macro game_hud} /// {@macro game_hud}

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/pinball_game.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
/// {@template play_button_overlay} /// {@template play_button_overlay}
@ -9,13 +9,7 @@ import 'package:pinball_ui/pinball_ui.dart';
/// {@endtemplate} /// {@endtemplate}
class PlayButtonOverlay extends StatelessWidget { class PlayButtonOverlay extends StatelessWidget {
/// {@macro play_button_overlay} /// {@macro play_button_overlay}
const PlayButtonOverlay({ const PlayButtonOverlay({Key? key}) : super(key: key);
Key? key,
required PinballGame game,
}) : _game = game,
super(key: key);
final PinballGame _game;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -23,9 +17,8 @@ class PlayButtonOverlay extends StatelessWidget {
return PinballButton( return PinballButton(
text: l10n.play, text: l10n.play,
onTap: () async { onTap: () {
_game.gameFlowController.start(); context.read<StartGameBloc>().add(const PlayTapped());
await showCharacterSelectionDialog(context);
}, },
); );
} }

@ -69,7 +69,7 @@ class _ScoreText extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final score = context.select((GameBloc bloc) => bloc.state.score); final score = context.select((GameBloc bloc) => bloc.state.displayScore);
return Text( return Text(
score.formatScore(), score.formatScore(),

@ -3,10 +3,8 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/gen/gen.dart'; import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
import 'package:platform_helper/platform_helper.dart'; import 'package:platform_helper/platform_helper.dart';
@ -51,24 +49,16 @@ extension on Control {
} }
} }
Future<void> showHowToPlayDialog(BuildContext context) {
final audio = context.read<PinballAudio>();
return showDialog<void>(
context: context,
builder: (_) => HowToPlayDialog(),
).then((_) {
audio.ioPinballVoiceOver();
});
}
class HowToPlayDialog extends StatefulWidget { class HowToPlayDialog extends StatefulWidget {
HowToPlayDialog({ HowToPlayDialog({
Key? key, Key? key,
required this.onDismissCallback,
@visibleForTesting PlatformHelper? platformHelper, @visibleForTesting PlatformHelper? platformHelper,
}) : platformHelper = platformHelper ?? PlatformHelper(), }) : platformHelper = platformHelper ?? PlatformHelper(),
super(key: key); super(key: key);
final PlatformHelper platformHelper; final PlatformHelper platformHelper;
final VoidCallback onDismissCallback;
@override @override
State<HowToPlayDialog> createState() => _HowToPlayDialogState(); State<HowToPlayDialog> createState() => _HowToPlayDialogState();
@ -82,6 +72,7 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
closeTimer = Timer(const Duration(seconds: 3), () { closeTimer = Timer(const Duration(seconds: 3), () {
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
widget.onDismissCallback.call();
} }
}); });
} }
@ -96,10 +87,17 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = widget.platformHelper.isMobile; final isMobile = widget.platformHelper.isMobile;
final l10n = context.l10n; final l10n = context.l10n;
return PinballDialog(
title: l10n.howToPlay, return WillPopScope(
subtitle: l10n.tipsForFlips, onWillPop: () {
child: isMobile ? const _MobileBody() : const _DesktopBody(), widget.onDismissCallback.call();
return Future.value(true);
},
child: PinballDialog(
title: l10n.howToPlay,
subtitle: l10n.tipsForFlips,
child: isMobile ? const _MobileBody() : const _DesktopBody(),
),
); );
} }
} }

@ -64,49 +64,45 @@
"@gameOver": { "@gameOver": {
"description": "Text displayed on the ending dialog when game finishes" "description": "Text displayed on the ending dialog when game finishes"
}, },
"leaderboard": "Leaderboard", "rounds": "Ball Ct:",
"@leaderboard": { "@rounds": {
"description": "Text displayed on the ending dialog leaderboard button" "description": "Text displayed on the scoreboard widget to indicate rounds left"
}, },
"rank": "Rank", "topPlayers": "Top Players",
"@rank": { "@topPlayers": {
"description": "Text displayed on the leaderboard page header rank column" "description": "Title text displayed on leaderboard screen"
}, },
"character": "Character", "rank": "rank",
"@character": { "@rank": {
"description": "Text displayed on the leaderboard page header character column" "description": "Label text displayed above player's rank"
}, },
"username": "Username", "name": "name",
"@username": { "@name": {
"description": "Text displayed on the leaderboard page header userName column" "description": "Label text displayed above player's initials"
}, },
"score": "Score", "score": "score",
"@score": { "@score": {
"description": "Text displayed on the leaderboard page header score column" "description": "Label text displayed above player's score"
},
"retry": "Retry",
"@retry": {
"description": "Text displayed on the retry button leaders board page"
}, },
"addUser": "Add User", "enterInitials": "Enter your initials using the",
"@addUser": { "@enterInitials": {
"description": "Text displayed on the add user button at ending dialog" "description": "Informational text displayed on initials input screen"
}, },
"error": "Error", "arrows": "arrows",
"@error": { "@arrows": {
"description": "Text displayed on the ending dialog when there is any error on sending user" "description": "Text displayed on initials input screen indicating arrow keys"
}, },
"yourScore": "Your score is", "andPress": "and press",
"@yourScore": { "@andPress": {
"description": "Text displayed on the ending dialog when game finishes to show the final score" "description": "Connecting text displayed on initials input screen informational text span"
}, },
"enterInitials": "Enter your initials", "enterReturn": "enter/return",
"@enterInitials": { "@enterReturn": {
"description": "Text displayed on the ending dialog when game finishes to ask the user for his initials" "description": "Text displayed on initials input screen indicating return key"
}, },
"rounds": "Ball Ct:", "toSubmit": "to submit",
"@rounds": { "@toSubmit": {
"description": "Text displayed on the scoreboard widget to indicate rounds left" "description": "Ending text displayed on initials input screen informational text span"
}, },
"footerMadeWithText": "Made with ", "footerMadeWithText": "Made with ",
"@footerMadeWithText": { "@footerMadeWithText": {

@ -1,20 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
/// Inflates [CharacterSelectionDialog] using [showDialog].
Future<void> showCharacterSelectionDialog(BuildContext context) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => const CharacterSelectionDialog(),
);
}
/// {@template character_selection_dialog} /// {@template character_selection_dialog}
/// Dialog used to select the playing character of the game. /// Dialog used to select the playing character of the game.
/// {@endtemplate character_selection_dialog} /// {@endtemplate character_selection_dialog}
@ -59,7 +50,7 @@ class _SelectCharacterButton extends StatelessWidget {
return PinballButton( return PinballButton(
onTap: () async { onTap: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
await showHowToPlayDialog(context); context.read<StartGameBloc>().add(const CharacterSelected());
}, },
text: l10n.select, text: l10n.select,
); );
@ -74,36 +65,40 @@ class _CharacterGrid extends StatelessWidget {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Column( Expanded(
children: [ child: Column(
_Character( children: [
key: const Key('sparky_character_selection'), _Character(
character: const SparkyTheme(), key: const Key('sparky_character_selection'),
isSelected: state.isSparkySelected, character: const SparkyTheme(),
), isSelected: state.isSparkySelected,
const SizedBox(height: 6), ),
_Character( const SizedBox(height: 6),
key: const Key('android_character_selection'), _Character(
character: const AndroidTheme(), key: const Key('android_character_selection'),
isSelected: state.isAndroidSelected, character: const AndroidTheme(),
), isSelected: state.isAndroidSelected,
], ),
],
),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Column( Expanded(
children: [ child: Column(
_Character( children: [
key: const Key('dash_character_selection'), _Character(
character: const DashTheme(), key: const Key('dash_character_selection'),
isSelected: state.isDashSelected, character: const DashTheme(),
), isSelected: state.isDashSelected,
const SizedBox(height: 6), ),
_Character( const SizedBox(height: 6),
key: const Key('dino_character_selection'), _Character(
character: const DinoTheme(), key: const Key('dino_character_selection'),
isSelected: state.isDinoSelected, character: const DinoTheme(),
), isSelected: state.isDinoSelected,
], ),
],
),
), ),
], ],
); );

@ -1,6 +1,5 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:pinball/game/game.dart';
part 'start_game_event.dart'; part 'start_game_event.dart';
part 'start_game_state.dart'; part 'start_game_state.dart';
@ -10,23 +9,16 @@ part 'start_game_state.dart';
/// {@endtemplate} /// {@endtemplate}
class StartGameBloc extends Bloc<StartGameEvent, StartGameState> { class StartGameBloc extends Bloc<StartGameEvent, StartGameState> {
/// {@macro start_game_bloc} /// {@macro start_game_bloc}
StartGameBloc({ StartGameBloc() : super(const StartGameState.initial()) {
required PinballGame game,
}) : _game = game,
super(const StartGameState.initial()) {
on<PlayTapped>(_onPlayTapped); on<PlayTapped>(_onPlayTapped);
on<CharacterSelected>(_onCharacterSelected); on<CharacterSelected>(_onCharacterSelected);
on<HowToPlayFinished>(_onHowToPlayFinished); on<HowToPlayFinished>(_onHowToPlayFinished);
} }
final PinballGame _game;
void _onPlayTapped( void _onPlayTapped(
PlayTapped event, PlayTapped event,
Emitter<StartGameState> emit, Emitter<StartGameState> emit,
) { ) {
_game.gameFlowController.start();
emit( emit(
state.copyWith( state.copyWith(
status: StartGameStatus.selectCharacter, status: StartGameStatus.selectCharacter,

@ -1 +1,2 @@
export 'bloc/start_game_bloc.dart'; export 'bloc/start_game_bloc.dart';
export 'widgets/start_game_listener.dart';

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template start_game_listener}
/// Listener that manages the display of dialogs for [StartGameStatus].
///
/// It's responsible for starting the game after pressing play button
/// and playing a sound after the 'how to play' dialog.
/// {@endtemplate}
class StartGameListener extends StatelessWidget {
/// {@macro start_game_listener}
const StartGameListener({
Key? key,
required Widget child,
required PinballGame game,
}) : _child = child,
_game = game,
super(key: key);
final Widget _child;
final PinballGame _game;
@override
Widget build(BuildContext context) {
return BlocListener<StartGameBloc, StartGameState>(
listener: (context, state) {
switch (state.status) {
case StartGameStatus.initial:
break;
case StartGameStatus.selectCharacter:
_onSelectCharacter(context);
_game.gameFlowController.start();
break;
case StartGameStatus.howToPlay:
_onHowToPlay(context);
break;
case StartGameStatus.play:
break;
}
},
child: _child,
);
}
void _onSelectCharacter(BuildContext context) {
_showPinballDialog(
context: context,
child: const CharacterSelectionDialog(),
barrierDismissible: false,
);
}
void _onHowToPlay(BuildContext context) {
final audio = context.read<PinballAudio>();
_showPinballDialog(
context: context,
child: HowToPlayDialog(
onDismissCallback: () {
context.read<StartGameBloc>().add(const HowToPlayFinished());
audio.ioPinballVoiceOver();
},
),
);
}
void _showPinballDialog({
required BuildContext context,
required Widget child,
bool barrierDismissible = true,
}) {
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
showDialog<void>(
context: context,
barrierColor: PinballColors.transparent,
barrierDismissible: barrierDismissible,
builder: (_) {
return Center(
child: SizedBox(
height: gameWidgetWidth * 0.87,
width: gameWidgetWidth,
child: child,
),
);
},
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

@ -11,7 +11,7 @@ class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
$AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen(); $AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBackboxGen get backbox => const $AssetsImagesBackboxGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
@ -50,20 +50,16 @@ class $AssetsImagesAndroidGen {
const $AssetsImagesAndroidSpaceshipGen(); const $AssetsImagesAndroidSpaceshipGen();
} }
class $AssetsImagesBackboardGen { class $AssetsImagesBackboxGen {
const $AssetsImagesBackboardGen(); const $AssetsImagesBackboxGen();
/// File path: assets/images/backboard/backboard_game_over.png /// File path: assets/images/backbox/display-divider.png
AssetGenImage get backboardGameOver => AssetGenImage get displayDivider =>
const AssetGenImage('assets/images/backboard/backboard_game_over.png'); const AssetGenImage('assets/images/backbox/display-divider.png');
/// File path: assets/images/backboard/backboard_scores.png /// File path: assets/images/backbox/marquee.png
AssetGenImage get backboardScores => AssetGenImage get marquee =>
const AssetGenImage('assets/images/backboard/backboard_scores.png'); const AssetGenImage('assets/images/backbox/marquee.png');
/// File path: assets/images/backboard/display.png
AssetGenImage get display =>
const AssetGenImage('assets/images/backboard/display.png');
} }
class $AssetsImagesBallGen { class $AssetsImagesBallGen {

@ -78,11 +78,11 @@ class AndroidBumper extends BodyComponent with InitialPosition, ZIndex {
AndroidBumper.cow({ AndroidBumper.cow({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
majorRadius: 3.4, majorRadius: 3.45,
minorRadius: 2.9, minorRadius: 3.11,
litAssetPath: Assets.images.android.bumper.cow.lit.keyName, litAssetPath: Assets.images.android.bumper.cow.lit.keyName,
dimmedAssetPath: Assets.images.android.bumper.cow.dimmed.keyName, dimmedAssetPath: Assets.images.android.bumper.cow.dimmed.keyName,
spritePosition: Vector2(0, -0.68), spritePosition: Vector2(0, -0.35),
bloc: AndroidBumperCubit(), bloc: AndroidBumperCubit(),
children: [ children: [
...?children, ...?children,

@ -1,79 +0,0 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
export 'backboard_game_over.dart';
export 'backboard_letter_prompt.dart';
export 'backboard_waiting.dart';
/// {@template backboard}
/// The [Backboard] of the pinball machine.
/// {@endtemplate}
class Backboard extends PositionComponent with HasGameRef {
/// {@macro backboard}
Backboard({
required Vector2 position,
}) : super(
position: position,
anchor: Anchor.bottomCenter,
);
/// {@macro backboard}
///
/// Returns a [Backboard] initialized in the waiting mode
factory Backboard.waiting({
required Vector2 position,
}) {
return Backboard(position: position)..waitingMode();
}
/// {@macro backboard}
///
/// Returns a [Backboard] initialized in the game over mode
factory Backboard.gameOver({
required Vector2 position,
required String characterIconPath,
required int score,
required BackboardOnSubmit onSubmit,
}) {
return Backboard(position: position)
..gameOverMode(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
);
}
/// [TextPaint] used on the [Backboard]
static final textPaint = TextPaint(
style: const TextStyle(
fontSize: 6,
color: Colors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// Puts the Backboard in waiting mode, where the scoreboard is shown.
Future<void> waitingMode() async {
children.removeWhere((_) => true);
await add(BackboardWaiting());
}
/// Puts the Backboard in game over mode, where the score input is shown.
Future<void> gameOverMode({
required int score,
required String characterIconPath,
BackboardOnSubmit? onSubmit,
}) async {
children.removeWhere((_) => true);
await add(
BackboardGameOver(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
),
);
}
}

@ -1,144 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Signature for the callback called when the used has
/// submettied their initials on the [BackboardGameOver]
typedef BackboardOnSubmit = void Function(String);
/// {@template backboard_game_over}
/// [PositionComponent] that handles the user input on the
/// game over display view.
/// {@endtemplate}
class BackboardGameOver extends PositionComponent with HasGameRef {
/// {@macro backboard_game_over}
BackboardGameOver({
required int score,
required String characterIconPath,
BackboardOnSubmit? onSubmit,
}) : _onSubmit = onSubmit,
super(
children: [
_BackboardSpriteComponent(),
_BackboardDisplaySpriteComponent(),
_ScoreTextComponent(score.formatScore()),
_CharacterIconSpriteComponent(characterIconPath),
],
);
final BackboardOnSubmit? _onSubmit;
@override
Future<void> onLoad() async {
for (var i = 0; i < 3; i++) {
await add(
BackboardLetterPrompt(
position: Vector2(
24.3 + (4.5 * i),
-45,
),
hasFocus: i == 0,
),
);
}
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowLeft: () => _movePrompt(true),
LogicalKeyboardKey.arrowRight: () => _movePrompt(false),
LogicalKeyboardKey.enter: _submit,
},
),
);
}
/// Returns the current inputed initials
String get initials => children
.whereType<BackboardLetterPrompt>()
.map((prompt) => prompt.char)
.join();
bool _submit() {
_onSubmit?.call(initials);
return true;
}
bool _movePrompt(bool left) {
final prompts = children.whereType<BackboardLetterPrompt>().toList();
final current = prompts.firstWhere((prompt) => prompt.hasFocus)
..hasFocus = false;
var index = prompts.indexOf(current) + (left ? -1 : 1);
index = min(max(0, index), prompts.length - 1);
prompts[index].hasFocus = true;
return false;
}
}
class _BackboardSpriteComponent extends SpriteComponent with HasGameRef {
_BackboardSpriteComponent() : super(anchor: Anchor.bottomCenter);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardGameOver.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _BackboardDisplaySpriteComponent extends SpriteComponent with HasGameRef {
_BackboardDisplaySpriteComponent()
: super(
anchor: Anchor.bottomCenter,
position: Vector2(0, -11.5),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.backboard.display.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _ScoreTextComponent extends TextComponent {
_ScoreTextComponent(String score)
: super(
text: score,
anchor: Anchor.centerLeft,
position: Vector2(-34, -45),
textRenderer: Backboard.textPaint,
);
}
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
_CharacterIconSpriteComponent(String characterIconPath)
: _characterIconPath = characterIconPath,
super(
anchor: Anchor.center,
position: Vector2(18.4, -45),
);
final String _characterIconPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

@ -1,102 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template backboard_letter_prompt}
/// A [PositionComponent] that renders a letter prompt used
/// on the [BackboardGameOver]
/// {@endtemplate}
class BackboardLetterPrompt extends PositionComponent {
/// {@macro backboard_letter_prompt}
BackboardLetterPrompt({
required Vector2 position,
bool hasFocus = false,
}) : _hasFocus = hasFocus,
super(
position: position,
);
static const _alphabetCode = 65;
static const _alphabetLength = 25;
var _charIndex = 0;
bool _hasFocus;
late RectangleComponent _underscore;
late TextComponent _input;
late TimerComponent _underscoreBlinker;
@override
Future<void> onLoad() async {
_underscore = RectangleComponent(
size: Vector2(3.8, 0.8),
anchor: Anchor.center,
position: Vector2(-0.3, 4),
);
await add(_underscore);
_input = TextComponent(
text: 'A',
textRenderer: Backboard.textPaint,
anchor: Anchor.center,
);
await add(_input);
_underscoreBlinker = TimerComponent(
period: 0.6,
repeat: true,
autoStart: _hasFocus,
onTick: () {
_underscore.paint.color = (_underscore.paint.color == Colors.white)
? Colors.transparent
: Colors.white;
},
);
await add(_underscoreBlinker);
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowUp: () => _cycle(true),
LogicalKeyboardKey.arrowDown: () => _cycle(false),
},
),
);
}
/// Returns the current selected character
String get char => String.fromCharCode(_alphabetCode + _charIndex);
bool _cycle(bool up) {
if (_hasFocus) {
final newCharCode =
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength);
_input.text = String.fromCharCode(_alphabetCode + newCharCode);
_charIndex = newCharCode;
return false;
}
return true;
}
/// Returns if this prompt has focus on it
bool get hasFocus => _hasFocus;
/// Updates this prompt focus
set hasFocus(bool hasFocus) {
if (hasFocus) {
_underscoreBlinker.timer.resume();
} else {
_underscoreBlinker.timer.pause();
}
_underscore.paint.color = Colors.white;
_hasFocus = hasFocus;
}
}

@ -1,17 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// [PositionComponent] that shows the leaderboard while the player
/// has not started the game yet.
class BackboardWaiting extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardScores.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.bottomCenter;
}
}

@ -1,14 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/ball/behaviors/ball_gravitating_behavior.dart';
import 'package:pinball_components/src/components/ball/behaviors/ball_scaling_behavior.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'behaviors/behaviors.dart';
/// {@template ball} /// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around. /// A solid, [BodyType.dynamic] sphere that rolls and bounces around.
/// {@endtemplate} /// {@endtemplate}
@ -50,17 +49,13 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
@override @override
Body createBody() { Body createBody() {
final shape = CircleShape()..radius = size.x / 2; final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(
shape,
density: 1,
);
final bodyDef = BodyDef( final bodyDef = BodyDef(
position: initialPosition, position: initialPosition,
userData: this,
type: BodyType.dynamic, type: BodyType.dynamic,
userData: this,
); );
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixtureFromShape(shape, 1);
} }
/// Immediatly and completly [stop]s the ball. /// Immediatly and completly [stop]s the ball.
@ -81,12 +76,6 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
void resume() { void resume() {
body.gravityScale = Vector2(1, 1); body.gravityScale = Vector2(1, 1);
} }
/// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball].
Future<void> boost(Vector2 impulse) async {
body.linearVelocity = impulse;
await add(_TurboChargeSpriteAnimationComponent());
}
} }
class _BallSpriteComponent extends SpriteComponent with HasGameRef { class _BallSpriteComponent extends SpriteComponent with HasGameRef {
@ -101,55 +90,3 @@ class _BallSpriteComponent extends SpriteComponent with HasGameRef {
anchor = Anchor.center; anchor = Anchor.center;
} }
} }
class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef, ZIndex {
_TurboChargeSpriteAnimationComponent()
: super(
anchor: const Anchor(0.53, 0.72),
removeOnFinish: true,
) {
zIndex = ZIndexes.turboChargeFlame;
}
late final Vector2 _textureSize;
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.ball.flameEffect.keyName,
);
const amountPerRow = 8;
const amountPerColumn = 4;
_textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: _textureSize,
loop: false,
),
);
}
@override
void update(double dt) {
super.update(dt);
if (parent != null) {
final body = (parent! as BodyComponent).body;
final direction = -body.linearVelocity.normalized();
angle = math.atan2(direction.x, -direction.y);
size = (_textureSize / 45) * body.fixtures.first.shape.radius;
}
}
}

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

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

@ -1,7 +1,6 @@
export 'android_animatronic.dart'; export 'android_animatronic.dart';
export 'android_bumper/android_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'android_spaceship/android_spaceship.dart'; export 'android_spaceship/android_spaceship.dart';
export 'backboard/backboard.dart';
export 'ball/ball.dart'; export 'ball/ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board_background_sprite_component.dart'; export 'board_background_sprite_component.dart';
@ -32,7 +31,7 @@ export 'shapes/shapes.dart';
export 'signpost/signpost.dart'; export 'signpost/signpost.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export 'spaceship_ramp.dart'; export 'spaceship_ramp/spaceship_ramp.dart';
export 'sparky_animatronic.dart'; export 'sparky_animatronic.dart';
export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_bumper/sparky_bumper.dart';
export 'sparky_computer.dart'; export 'sparky_computer.dart';

@ -23,16 +23,20 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
ScoreComponent({ ScoreComponent({
required this.points, required this.points,
required Vector2 position, required Vector2 position,
}) : super( required EffectController effectController,
}) : _effectController = effectController,
super(
position: position, position: position,
anchor: Anchor.center, anchor: Anchor.center,
) { ) {
zIndex = ZIndexes.score; zIndex = ZIndexes.score;
} }
late Points points;
late final Effect _effect; late final Effect _effect;
late Points points; final EffectController _effectController;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -46,7 +50,7 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
await add( await add(
_effect = MoveEffect.by( _effect = MoveEffect.by(
Vector2(0, -5), Vector2(0, -5),
EffectController(duration: 1), _effectController,
), ),
); );
} }

@ -0,0 +1,24 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_ball_ascending_contact_behavior}
/// Detects an ascending [Ball] that enters into the [SpaceshipRamp].
///
/// The [Ball] can hit with sensor to recognize if a [Ball] goes into or out of
/// the [SpaceshipRamp].
/// {@endtemplate}
class RampBallAscendingContactBehavior
extends ContactBehavior<RampScoringSensor> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.body.linearVelocity.y < 0) {
parent.parent.bloc.onAscendingBallEntered();
}
}
}

@ -0,0 +1,16 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'spaceship_ramp_state.dart';
class SpaceshipRampCubit extends Cubit<SpaceshipRampState> {
SpaceshipRampCubit() : super(const SpaceshipRampState.initial());
void onAscendingBallEntered() {
emit(
state.copyWith(hits: state.hits + 1),
);
}
}

@ -0,0 +1,24 @@
// ignore_for_file: public_member_api_docs
part of 'spaceship_ramp_cubit.dart';
class SpaceshipRampState extends Equatable {
const SpaceshipRampState({
required this.hits,
}) : assert(hits >= 0, "Hits can't be negative");
const SpaceshipRampState.initial() : this(hits: 0);
final int hits;
SpaceshipRampState copyWith({
int? hits,
}) {
return SpaceshipRampState(
hits: hits ?? this.hits,
);
}
@override
List<Object?> get props => [hits];
}

@ -5,16 +5,35 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/spaceship_ramp_cubit.dart';
/// {@template spaceship_ramp} /// {@template spaceship_ramp}
/// Ramp leading into the [AndroidSpaceship]. /// Ramp leading into the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRamp extends Component { class SpaceshipRamp extends Component {
/// {@macro spaceship_ramp} /// {@macro spaceship_ramp}
SpaceshipRamp() SpaceshipRamp({
: super( Iterable<Component>? children,
}) : this._(
children: children,
bloc: SpaceshipRampCubit(),
);
SpaceshipRamp._({
Iterable<Component>? children,
required this.bloc,
}) : super(
children: [ children: [
// TODO(ruimiguel): refactor RampScoringSensor and
// _SpaceshipRampOpening to be in only one sensor if possible.
RampScoringSensor(
children: [
RampBallAscendingContactBehavior(),
],
)..initialPosition = Vector2(1.7, -20.4),
_SpaceshipRampOpening( _SpaceshipRampOpening(
outsidePriority: ZIndexes.ballOnBoard, outsidePriority: ZIndexes.ballOnBoard,
rotation: math.pi, rotation: math.pi,
@ -34,60 +53,30 @@ class SpaceshipRamp extends Component {
_SpaceshipRampForegroundRailing(), _SpaceshipRampForegroundRailing(),
_SpaceshipRampBase()..initialPosition = Vector2(1.7, -20), _SpaceshipRampBase()..initialPosition = Vector2(1.7, -20),
_SpaceshipRampBackgroundRailingSpriteComponent(), _SpaceshipRampBackgroundRailingSpriteComponent(),
_SpaceshipRampArrowSpriteComponent(), SpaceshipRampArrowSpriteComponent(
current: bloc.state.hits,
),
...?children,
], ],
); );
/// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. /// Creates a [SpaceshipRamp] without any children.
/// ///
/// If the current state is the last one it cycles back to the initial state. /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation.
void progress() => @visibleForTesting
firstChild<_SpaceshipRampArrowSpriteComponent>()?.progress(); SpaceshipRamp.test({
} required this.bloc,
}) : super();
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up. // TODO(alestiago): Consider refactoring once the following is merged:
active2, // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
/// Arrow with 3 lights lit up. final SpaceshipRampCubit bloc;
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState {
String get path {
switch (this) {
case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
return Assets.images.android.ramp.arrow.active5.keyName;
}
}
SpaceshipRampArrowSpriteState get next { @override
return SpaceshipRampArrowSpriteState void onRemove() {
.values[(index + 1) % SpaceshipRampArrowSpriteState.values.length]; bloc.close();
super.onRemove();
} }
} }
@ -194,37 +183,81 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
/// ///
/// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class _SpaceshipRampArrowSpriteComponent @visibleForTesting
extends SpriteGroupComponent<SpaceshipRampArrowSpriteState> class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent<int>
with HasGameRef, ZIndex { with HasGameRef, ParentIsA<SpaceshipRamp>, ZIndex {
/// {@macro spaceship_ramp_arrow_sprite_component} /// {@macro spaceship_ramp_arrow_sprite_component}
_SpaceshipRampArrowSpriteComponent() SpaceshipRampArrowSpriteComponent({
: super( required int current,
}) : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-3.9, -56.5), position: Vector2(-3.9, -56.5),
current: current,
) { ) {
zIndex = ZIndexes.spaceshipRampArrow; zIndex = ZIndexes.spaceshipRampArrow;
} }
/// Changes arrow image to the next [Sprite].
void progress() => current = current?.next;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprites = <SpaceshipRampArrowSpriteState, Sprite>{}; parent.bloc.stream.listen((state) {
current = state.hits % SpaceshipRampArrowSpriteState.values.length;
});
final sprites = <int, Sprite>{};
this.sprites = sprites; this.sprites = sprites;
for (final spriteState in SpaceshipRampArrowSpriteState.values) { for (final spriteState in SpaceshipRampArrowSpriteState.values) {
sprites[spriteState] = Sprite( sprites[spriteState.index] = Sprite(
gameRef.images.fromCache(spriteState.path), gameRef.images.fromCache(spriteState.path),
); );
} }
current = SpaceshipRampArrowSpriteState.inactive; current = 0;
size = sprites[current]!.originalSize / 10; size = sprites[current]!.originalSize / 10;
} }
} }
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState {
String get path {
switch (this) {
case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
return Assets.images.android.ramp.arrow.active5.keyName;
}
}
}
class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex { with HasGameRef, ZIndex {
_SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) { _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) {
@ -373,3 +406,47 @@ class _SpaceshipRampOpening extends LayerSensor {
); );
} }
} }
/// {@template ramp_scoring_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SpaceshipRamp].
/// {@endtemplate}
class RampScoringSensor extends BodyComponent
with ParentIsA<SpaceshipRamp>, InitialPosition, Layered {
/// {@macro ramp_scoring_sensor}
RampScoringSensor({
Iterable<Component>? children,
}) : super(
children: children,
renderBody: false,
) {
layer = Layer.spaceshipEntranceRamp;
}
/// Creates a [RampScoringSensor] without any children.
///
@visibleForTesting
RampScoringSensor.test();
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
2.6,
.5,
initialPosition,
-5 * math.pi / 180,
);
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -114,5 +114,10 @@ abstract class ZIndexes {
static const score = _above + spaceshipRampForegroundRailing; static const score = _above + spaceshipRampForegroundRailing;
// Debug information // Debug information
static const debugInfo = _above + score; static const debugInfo = _above + score;
// Backbox
static const backbox = _below + outerBoundary;
} }

@ -73,7 +73,6 @@ flutter:
- assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/a/
- assets/images/sparky/bumper/b/ - assets/images/sparky/bumper/b/
- assets/images/sparky/bumper/c/ - assets/images/sparky/bumper/c/
- assets/images/backboard/
- assets/images/google_word/letter1/ - assets/images/google_word/letter1/
- assets/images/google_word/letter2/ - assets/images/google_word/letter2/
- assets/images/google_word/letter3/ - assets/images/google_word/letter3/
@ -88,6 +87,7 @@ flutter:
- assets/images/multiplier/x5/ - assets/images/multiplier/x5/
- assets/images/multiplier/x6/ - assets/images/multiplier/x6/
- assets/images/score/ - assets/images/score/
- assets/images/backbox/
- assets/images/flapper/ - assets/images/flapper/
flutter_gen: flutter_gen:

@ -18,7 +18,6 @@ void main() {
addGoogleWordStories(dashbook); addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook); addLaunchRampStories(dashbook);
addScoreStories(dashbook); addScoreStories(dashbook);
addBackboardStories(dashbook);
addMultiballStories(dashbook); addMultiballStories(dashbook);
addMultipliersStories(dashbook); addMultipliersStories(dashbook);

@ -54,7 +54,7 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
) { ) {
if (event is RawKeyDownEvent && if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) { event.logicalKey == LogicalKeyboardKey.space) {
_spaceshipRamp.progress(); _spaceshipRamp.bloc.onAscendingBallEntered();
return KeyEventResult.handled; return KeyEventResult.handled;
} }

@ -1,61 +0,0 @@
import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_theme/pinball_theme.dart';
import 'package:sandbox/common/common.dart';
class BackboardGameOverGame extends AssetsGame
with HasKeyboardHandlerComponents {
BackboardGameOverGame(this.score, this.character)
: super(
imagesFileNames: [
components.Assets.images.score.fiveThousand.keyName,
components.Assets.images.score.twentyThousand.keyName,
components.Assets.images.score.twoHundredThousand.keyName,
components.Assets.images.score.oneMillion.keyName,
...characterIconPaths.values.toList(),
],
);
static const description = '''
Shows how the Backboard in game over mode is rendered.
- Select a character to update the character icon.
''';
static final characterIconPaths = <String, String>{
'Dash': Assets.images.dash.leaderboardIcon.keyName,
'Sparky': Assets.images.sparky.leaderboardIcon.keyName,
'Android': Assets.images.android.leaderboardIcon.keyName,
'Dino': Assets.images.dino.leaderboardIcon.keyName,
};
final int score;
final String character;
@override
Future<void> onLoad() async {
await super.onLoad();
camera
..followVector2(Vector2.zero())
..zoom = 5;
await add(
components.Backboard.gameOver(
position: Vector2(0, 20),
score: score,
characterIconPath: characterIconPaths[character]!,
onSubmit: (initials) {
add(
components.ScoreComponent(
points: components.Points.values
.firstWhere((element) => element.value == score),
position: Vector2(0, 50),
),
);
},
),
);
}
}

@ -1,25 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BackboardWaitingGame extends AssetsGame {
BackboardWaitingGame()
: super(
imagesFileNames: [],
);
static const description = '''
Shows how the Backboard in waiting mode is rendered.
''';
@override
Future<void> onLoad() async {
camera
..followVector2(Vector2.zero())
..zoom = 5;
await add(
Backboard.waiting(position: Vector2(0, 20)),
);
}
}

@ -1,30 +0,0 @@
import 'package:dashbook/dashbook.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/backboard/backboard_game_over_game.dart';
import 'package:sandbox/stories/backboard/backboard_waiting_game.dart';
void addBackboardStories(Dashbook dashbook) {
dashbook.storiesOf('Backboard')
..addGame(
title: 'Waiting',
description: BackboardWaitingGame.description,
gameBuilder: (_) => BackboardWaitingGame(),
)
..addGame(
title: 'Game over',
description: BackboardGameOverGame.description,
gameBuilder: (context) => BackboardGameOverGame(
context.listProperty(
'Score',
Points.values.first.value,
Points.values.map((score) => score.value).toList(),
),
context.listProperty(
'Character',
BackboardGameOverGame.characterIconPaths.keys.first,
BackboardGameOverGame.characterIconPaths.keys.toList(),
),
),
);
}

@ -13,8 +13,9 @@ class BallBoosterGame extends LineGame {
@override @override
void onLine(Vector2 line) { void onLine(Vector2 line) {
final ball = Ball(baseColor: Colors.transparent); final ball = Ball(baseColor: Colors.transparent);
add(ball); final impulse = line * -1 * 20;
ball.add(BallTurboChargingBehavior(impulse: impulse));
ball.mounted.then((value) => ball.boost(line * -1 * 20)); add(ball);
} }
} }

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:flame/effects.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
@ -38,6 +39,7 @@ class ScoreGame extends AssetsGame with TapDetector {
ScoreComponent( ScoreComponent(
points: score, points: score,
position: info.eventPosition.game..multiply(Vector2(1, -1)), position: info.eventPosition.game..multiply(Vector2(1, -1)),
effectController: EffectController(duration: 1),
), ),
); );
} }

@ -1,5 +1,4 @@
export 'android_acres/stories.dart'; export 'android_acres/stories.dart';
export 'backboard/stories.dart';
export 'ball/stories.dart'; export 'ball/stories.dart';
export 'bottom_group/stories.dart'; export 'bottom_group/stories.dart';
export 'boundaries/stories.dart'; export 'boundaries/stories.dart';

@ -1,185 +0,0 @@
// ignore_for_file: unawaited_futures, cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_theme/pinball_theme.dart';
import '../../helpers/helpers.dart';
void main() {
group('Backboard', () {
final characterIconPath = Assets.images.dash.leaderboardIcon.keyName;
final tester = FlameTester(() => KeyboardTestGame([characterIconPath]));
group('on waitingMode', () {
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(Backboard.waiting(position: Vector2(0, 15)));
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/backboard/waiting.png'),
);
},
);
});
group('on gameOverMode', () {
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero());
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(backboard);
},
verify: (game, tester) async {
final prompts =
game.descendants().whereType<BackboardLetterPrompt>().length;
expect(prompts, equals(3));
final score = game.descendants().firstWhere(
(component) =>
component is TextComponent && component.text == '1,000',
);
expect(score, isNotNull);
},
);
tester.testGameWidget(
'can change the initials',
setUp: (game, tester) async {
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(backboard);
// Focus is already on the first letter
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// Move to the next an press up again
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// One more time
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// Back to the previous and increase one more
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final backboard = game
.descendants()
.firstWhere((component) => component is BackboardGameOver)
as BackboardGameOver;
expect(backboard.initials, equals('BCB'));
},
);
String? submitedInitials;
tester.testGameWidget(
'submits the initials',
setUp: (game, tester) async {
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (value) {
submitedInitials = value;
},
);
await game.ensureAdd(backboard);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
},
verify: (game, tester) async {
expect(submitedInitials, equals('AAA'));
},
);
});
});
group('BackboardLetterPrompt', () {
final tester = FlameTester(KeyboardTestGame.new);
tester.testGameWidget(
'cycles the char up and down when it has focus',
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(hasFocus: true, position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<BackboardLetterPrompt>();
expect(prompt?.char, equals('C'));
},
);
tester.testGameWidget(
"does nothing when it doesn't have focus",
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<BackboardLetterPrompt>();
expect(prompt?.char, equals('A'));
},
);
tester.testGameWidget(
'blinks the prompt when it has the focus',
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(position: Vector2.zero(), hasFocus: true),
);
},
verify: (game, tester) async {
final underscore = game.descendants().whereType<ShapeComponent>().first;
expect(underscore.paint.color, Colors.white);
game.update(2);
expect(underscore.paint.color, Colors.transparent);
},
);
});
}

@ -1,12 +1,10 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
@ -180,50 +178,5 @@ void main() {
); );
}); });
}); });
group('boost', () {
flameTester.test('applies an impulse to the ball', (game) async {
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
expect(ball.body.linearVelocity, equals(Vector2.zero()));
await ball.boost(Vector2.all(10));
expect(ball.body.linearVelocity.x, greaterThan(0));
expect(ball.body.linearVelocity.y, greaterThan(0));
});
flameTester.test('adds TurboChargeSpriteAnimation', (game) async {
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
await ball.boost(Vector2.all(10));
game.update(0);
expect(
ball.children.whereType<SpriteAnimationComponent>().single,
isNotNull,
);
});
flameTester.test('removes TurboChargeSpriteAnimation after it finishes',
(game) async {
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
await ball.boost(Vector2.all(10));
game.update(0);
final turboChargeSpriteAnimation =
ball.children.whereType<SpriteAnimationComponent>().single;
expect(ball.contains(turboChargeSpriteAnimation), isTrue);
game.update(turboChargeSpriteAnimation.animation!.totalDuration());
game.update(0.1);
expect(ball.contains(turboChargeSpriteAnimation), isFalse);
});
});
}); });
} }

@ -6,7 +6,6 @@ import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';

@ -6,7 +6,6 @@ import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';

@ -0,0 +1,94 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group(
'BallTurboChargingBehavior',
() {
final assets = [Assets.images.ball.ball.keyName];
final flameTester = FlameTester(() => TestGame(assets));
const baseColor = Color(0xFFFFFFFF);
test('can be instantiated', () {
expect(
BallTurboChargingBehavior(impulse: Vector2.zero()),
isA<BallTurboChargingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final ball = Ball.test(baseColor: baseColor);
final behavior = BallTurboChargingBehavior(impulse: Vector2.zero());
await ball.add(behavior);
await game.ensureAdd(ball);
expect(
ball.firstChild<BallTurboChargingBehavior>(),
equals(behavior),
);
});
flameTester.test(
'impulses the ball velocity when loaded',
(game) async {
final ball = Ball.test(baseColor: baseColor);
await game.ensureAdd(ball);
final impulse = Vector2.all(1);
final behavior = BallTurboChargingBehavior(impulse: impulse);
await ball.ensureAdd(behavior);
expect(
ball.body.linearVelocity.x,
equals(impulse.x),
);
expect(
ball.body.linearVelocity.y,
equals(impulse.y),
);
},
);
flameTester.test('adds sprite', (game) async {
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
await ball.ensureAdd(
BallTurboChargingBehavior(impulse: Vector2.zero()),
);
expect(
ball.children.whereType<SpriteAnimationComponent>().single,
isNotNull,
);
});
flameTester.test('removes sprite after it finishes', (game) async {
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
final behavior = BallTurboChargingBehavior(impulse: Vector2.zero());
await ball.ensureAdd(behavior);
final turboChargeSpriteAnimation =
ball.children.whereType<SpriteAnimationComponent>().single;
expect(ball.contains(turboChargeSpriteAnimation), isTrue);
game.update(behavior.timer.limit);
game.update(0.1);
expect(ball.contains(turboChargeSpriteAnimation), isFalse);
});
},
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 80 KiB

@ -28,6 +28,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.oneMillion, points: Points.oneMillion,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
}, },
@ -46,6 +47,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.oneMillion, points: Points.oneMillion,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -67,6 +69,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.oneMillion, points: Points.oneMillion,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -88,6 +91,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.fiveThousand, points: Points.fiveThousand,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -113,6 +117,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.twentyThousand, points: Points.twentyThousand,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -138,6 +143,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.twoHundredThousand, points: Points.twoHundredThousand,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -163,6 +169,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.oneMillion, points: Points.oneMillion,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );

@ -0,0 +1,117 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.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 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import '../../../../helpers/helpers.dart';
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockBall extends Mock implements Ball {}
class _MockBody extends Mock implements Body {}
class _MockContact extends Mock implements Contact {}
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,
];
final flameTester = FlameTester(() => TestGame(assets));
group(
'RampBallAscendingContactBehavior',
() {
test('can be instantiated', () {
expect(
RampBallAscendingContactBehavior(),
isA<RampBallAscendingContactBehavior>(),
);
});
group('beginContact', () {
late Ball ball;
late Body body;
setUp(() {
ball = _MockBall();
body = _MockBody();
when(() => ball.body).thenReturn(body);
});
flameTester.test(
"calls 'onAscendingBallEntered' when a ball enters into the ramp",
(game) async {
final behavior = RampBallAscendingContactBehavior();
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
final rampSensor = RampScoringSensor.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
);
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
await spaceshipRamp.add(rampSensor);
await game.ensureAddAll([spaceshipRamp, ball]);
await rampSensor.add(behavior);
behavior.beginContact(ball, _MockContact());
verify(bloc.onAscendingBallEntered).called(1);
},
);
flameTester.test(
"doesn't call 'onAscendingBallEntered' when a ball goes out the ramp",
(game) async {
final behavior = RampBallAscendingContactBehavior();
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
final rampSensor = RampScoringSensor.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
);
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
await spaceshipRamp.add(rampSensor);
await game.ensureAddAll([spaceshipRamp, ball]);
await rampSensor.add(behavior);
behavior.beginContact(ball, _MockContact());
verifyNever(bloc.onAscendingBallEntered);
},
);
});
},
);
}

@ -0,0 +1,25 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('SpaceshipRampCubit', () {
group('onAscendingBallEntered', () {
blocTest<SpaceshipRampCubit, SpaceshipRampState>(
'emits hits incremented and arrow goes to the next value',
build: SpaceshipRampCubit.new,
act: (bloc) => bloc
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered(),
expect: () => [
SpaceshipRampState(hits: 1),
SpaceshipRampState(hits: 2),
SpaceshipRampState(hits: 3),
],
);
});
});
}

@ -0,0 +1,78 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/components/components.dart';
void main() {
group('SpaceshipRampState', () {
test('supports value equality', () {
expect(
SpaceshipRampState(hits: 0),
equals(
SpaceshipRampState(hits: 0),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
SpaceshipRampState(hits: 0),
isNotNull,
);
});
});
test(
'throws AssertionError '
'when hits is negative',
() {
expect(
() => SpaceshipRampState(hits: -1),
throwsAssertionError,
);
},
);
group('copyWith', () {
test(
'throws AssertionError '
'when hits is decreased',
() {
const rampState = SpaceshipRampState(hits: 0);
expect(
() => rampState.copyWith(hits: rampState.hits - 1),
throwsAssertionError,
);
},
);
test(
'copies correctly '
'when no argument specified',
() {
const rampState = SpaceshipRampState(hits: 0);
expect(
rampState.copyWith(),
equals(rampState),
);
},
);
test(
'copies correctly '
'when all arguments specified',
() {
const rampState = SpaceshipRampState(hits: 0);
final otherRampState = SpaceshipRampState(hits: rampState.hits + 1);
expect(rampState, isNot(equals(otherRampState)));
expect(
rampState.copyWith(hits: rampState.hits + 1),
equals(otherRampState),
);
},
);
});
});
}

@ -1,12 +1,16 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -25,28 +29,35 @@ void main() {
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));
group('SpaceshipRamp', () { group('SpaceshipRamp', () {
flameTester.test('loads correctly', (game) async { flameTester.test(
final component = SpaceshipRamp(); 'loads correctly',
await game.ensureAdd(component); (game) async {
expect(game.contains(component), isTrue); final spaceshipRamp = SpaceshipRamp();
}); await game.ensureAdd(spaceshipRamp);
expect(game.children, contains(spaceshipRamp));
},
);
group('renders correctly', () { group('renders correctly', () {
const goldenFilePath = 'golden/spaceship_ramp/'; const goldenFilePath = '../golden/spaceship_ramp/';
final centerForSpaceshipRamp = Vector2(-13, -55); final centerForSpaceshipRamp = Vector2(-13, -55);
flameTester.testGameWidget( flameTester.testGameWidget(
'inactive sprite', 'inactive sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.inactive, SpaceshipRampArrowSpriteState.inactive,
); );
@ -64,15 +75,21 @@ void main() {
'active1 sprite', 'active1 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component.progress(); ramp.bloc.onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active1, SpaceshipRampArrowSpriteState.active1,
); );
@ -90,17 +107,23 @@ void main() {
'active2 sprite', 'active2 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component ramp.bloc
..progress() ..onAscendingBallEntered()
..progress(); ..onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active2, SpaceshipRampArrowSpriteState.active2,
); );
@ -118,18 +141,24 @@ void main() {
'active3 sprite', 'active3 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component ramp.bloc
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress(); ..onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active3, SpaceshipRampArrowSpriteState.active3,
); );
@ -147,19 +176,25 @@ void main() {
'active4 sprite', 'active4 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component ramp.bloc
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress(); ..onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active4, SpaceshipRampArrowSpriteState.active4,
); );
@ -177,20 +212,26 @@ void main() {
'active5 sprite', 'active5 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component ramp.bloc
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress(); ..onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active5, SpaceshipRampArrowSpriteState.active5,
); );
@ -204,5 +245,34 @@ void main() {
}, },
); );
}); });
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final ramp = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ramp);
game.remove(ramp);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final ramp = SpaceshipRamp(children: [component]);
await game.ensureAdd(ramp);
expect(ramp.children, contains(component));
});
});
}); });
} }

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('ScoreX', () {
test('formatScore correctly formats int', () {
expect(1000000.formatScore(), '1,000,000');
});
});
}

@ -6,6 +6,7 @@ abstract class PinballColors {
static const Color darkBlue = Color(0xFF0C32A4); static const Color darkBlue = Color(0xFF0C32A4);
static const Color yellow = Color(0xFFFFEE02); static const Color yellow = Color(0xFFFFEE02);
static const Color orange = Color(0xFFE5AB05); static const Color orange = Color(0xFFE5AB05);
static const Color red = Color(0xFFF03939);
static const Color blue = Color(0xFF4B94F6); static const Color blue = Color(0xFF4B94F6);
static const Color transparent = Color(0x00000000); static const Color transparent = Color(0x00000000);
static const Color loadingDarkRed = Color(0xFFE33B2D); static const Color loadingDarkRed = Color(0xFFE33B2D);

@ -20,6 +20,10 @@ void main() {
expect(PinballColors.orange, const Color(0xFFE5AB05)); expect(PinballColors.orange, const Color(0xFFE5AB05));
}); });
test('red is 0xFFF03939', () {
expect(PinballColors.red, const Color(0xFFF03939));
});
test('blue is 0xFF4B94F6', () { test('blue is 0xFF4B94F6', () {
expect(PinballColors.blue, const Color(0xFF4B94F6)); expect(PinballColors.blue, const Color(0xFF4B94F6));
}); });

@ -0,0 +1,50 @@
// ignore_for_file: cascade_invocations
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/behaviors.dart';
import 'package:pinball_audio/pinball_audio.dart';
import '../../helpers/helpers.dart';
class _TestBodyComponent extends BodyComponent {
@override
Body createBody() {
return world.createBody(BodyDef());
}
}
class _MockPinballAudio extends Mock implements PinballAudio {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('BumperNoisyBehavior', () {});
late PinballAudio audio;
final flameTester = FlameTester(
() => EmptyPinballTestGame(audio: audio),
);
setUp(() {
audio = _MockPinballAudio();
});
flameTester.testGameWidget(
'plays bumper sound',
setUp: (game, _) async {
final behavior = BumperNoisyBehavior();
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
behavior.beginContact(Object(), _MockContact());
},
verify: (_, __) async {
verify(audio.bumper).called(1);
},
);
}

@ -0,0 +1,211 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.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/behaviors.dart';
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 _TestBodyComponent extends BodyComponent {
@override
Body createBody() => world.createBody(BodyDef());
}
class _MockBall extends Mock implements Ball {}
class _MockBody extends Mock implements Body {}
class _MockGameBloc extends Mock implements GameBloc {}
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(() {
ball = _MockBall();
final ballBody = _MockBody();
when(() => ball.body).thenReturn(ballBody);
when(() => ballBody.position).thenReturn(Vector2.all(4));
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: [],
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
assets: assets,
);
group('ScoringBehavior', () {
test('can be instantiated', () {
expect(
ScoringBehavior(
points: Points.fiveThousand,
position: Vector2.zero(),
),
isA<ScoringBehavior>(),
);
});
flameBlocTester.testGameWidget(
'can be loaded',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
final behavior = ScoringBehavior(
points: Points.fiveThousand,
position: Vector2.zero(),
);
await parent.add(behavior);
await game.ensureAdd(canvas);
expect(
parent.firstChild<ScoringBehavior>(),
equals(behavior),
);
},
);
flameBlocTester.testGameWidget(
'emits Scored event with points when added',
setUp: (game, tester) async {
const points = Points.oneMillion;
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
final behavior = ScoringBehavior(
points: points,
position: Vector2(0, 0),
);
await parent.ensureAdd(behavior);
verify(
() => bloc.add(
Scored(points: points.value),
),
).called(1);
},
);
flameBlocTester.testGameWidget(
'correctly renders text',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
const points = Points.oneMillion;
final position = Vector2.all(1);
final behavior = ScoringBehavior(
points: points,
position: position,
);
await parent.ensureAdd(behavior);
final scoreText = game.descendants().whereType<ScoreComponent>();
expect(scoreText.length, equals(1));
expect(
scoreText.first.points,
equals(points),
);
expect(
scoreText.first.position,
equals(position),
);
},
);
flameBlocTester.testGameWidget(
'is removed after duration',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
const duration = 2.0;
final behavior = ScoringBehavior(
points: Points.oneMillion,
position: Vector2(0, 0),
duration: duration,
);
await parent.ensureAdd(behavior);
game.update(duration);
game.update(0);
await tester.pump();
},
verify: (game, _) async {
expect(
game.descendants().whereType<ScoringBehavior>(),
isEmpty,
);
},
);
});
group('ScoringContactBehavior', () {
flameBlocTester.testGameWidget(
'beginContact adds a ScoringBehavior',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
final behavior = ScoringContactBehavior(points: Points.oneMillion);
await parent.ensureAdd(behavior);
behavior.beginContact(ball, _MockContact());
await game.ready();
expect(
parent.firstChild<ScoringBehavior>(),
isNotNull,
);
},
);
flameBlocTester.testGameWidget(
"beginContact positions text at contact's position",
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
final behavior = ScoringContactBehavior(points: Points.oneMillion);
await parent.ensureAdd(behavior);
behavior.beginContact(ball, _MockContact());
await game.ready();
final scoreText = game.descendants().whereType<ScoreComponent>();
expect(
scoreText.first.position,
equals(ball.body.position),
);
},
);
});
}

@ -6,7 +6,7 @@ void main() {
group('GameBloc', () { group('GameBloc', () {
test('initial state has 3 rounds and empty score', () { test('initial state has 3 rounds and empty score', () {
final gameBloc = GameBloc(); final gameBloc = GameBloc();
expect(gameBloc.state.score, equals(0)); expect(gameBloc.state.roundScore, equals(0));
expect(gameBloc.state.rounds, equals(3)); expect(gameBloc.state.rounds, equals(3));
}); });
@ -19,21 +19,17 @@ void main() {
bloc.add(const RoundLost()); bloc.add(const RoundLost());
}, },
expect: () => [ expect: () => [
const GameState( isA<GameState>()..having((state) => state.rounds, 'rounds', 2),
score: 0,
multiplier: 1,
rounds: 2,
bonusHistory: [],
),
], ],
); );
blocTest<GameBloc, GameState>( blocTest<GameBloc, GameState>(
'apply multiplier to score ' 'apply multiplier to roundScore and add it to totalScore '
'when round is lost', 'when round is lost',
build: GameBloc.new, build: GameBloc.new,
seed: () => const GameState( seed: () => const GameState(
score: 5, totalScore: 10,
roundScore: 5,
multiplier: 3, multiplier: 3,
rounds: 2, rounds: 2,
bonusHistory: [], bonusHistory: [],
@ -43,8 +39,8 @@ void main() {
}, },
expect: () => [ expect: () => [
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 15) ..having((state) => state.totalScore, 'totalScore', 25)
..having((state) => state.rounds, 'rounds', 1), ..having((state) => state.roundScore, 'roundScore', 0)
], ],
); );
@ -53,7 +49,8 @@ void main() {
'when round is lost', 'when round is lost',
build: GameBloc.new, build: GameBloc.new,
seed: () => const GameState( seed: () => const GameState(
score: 5, totalScore: 10,
roundScore: 5,
multiplier: 3, multiplier: 3,
rounds: 2, rounds: 2,
bonusHistory: [], bonusHistory: [],
@ -62,9 +59,7 @@ void main() {
bloc.add(const RoundLost()); bloc.add(const RoundLost());
}, },
expect: () => [ expect: () => [
isA<GameState>() isA<GameState>()..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.rounds, 'rounds', 1),
], ],
); );
}); });
@ -79,10 +74,10 @@ void main() {
..add(const Scored(points: 3)), ..add(const Scored(points: 3)),
expect: () => [ expect: () => [
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 2) ..having((state) => state.roundScore, 'roundScore', 2)
..having((state) => state.isGameOver, 'isGameOver', false), ..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 5) ..having((state) => state.roundScore, 'roundScore', 5)
..having((state) => state.isGameOver, 'isGameOver', false), ..having((state) => state.isGameOver, 'isGameOver', false),
], ],
); );
@ -99,15 +94,15 @@ void main() {
}, },
expect: () => [ expect: () => [
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 0) ..having((state) => state.roundScore, 'roundScore', 0)
..having((state) => state.rounds, 'rounds', 2) ..having((state) => state.rounds, 'rounds', 2)
..having((state) => state.isGameOver, 'isGameOver', false), ..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 0) ..having((state) => state.roundScore, 'roundScore', 0)
..having((state) => state.rounds, 'rounds', 1) ..having((state) => state.rounds, 'rounds', 1)
..having((state) => state.isGameOver, 'isGameOver', false), ..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 0) ..having((state) => state.roundScore, 'roundScore', 0)
..having((state) => state.rounds, 'rounds', 0) ..having((state) => state.rounds, 'rounds', 0)
..having((state) => state.isGameOver, 'isGameOver', true), ..having((state) => state.isGameOver, 'isGameOver', true),
], ],
@ -124,11 +119,9 @@ void main() {
..add(const MultiplierIncreased()), ..add(const MultiplierIncreased()),
expect: () => [ expect: () => [
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 2) ..having((state) => state.multiplier, 'multiplier', 2)
..having((state) => state.isGameOver, 'isGameOver', false), ..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 3) ..having((state) => state.multiplier, 'multiplier', 3)
..having((state) => state.isGameOver, 'isGameOver', false), ..having((state) => state.isGameOver, 'isGameOver', false),
], ],
@ -139,7 +132,8 @@ void main() {
'when multiplier is 6 and game is not over', 'when multiplier is 6 and game is not over',
build: GameBloc.new, build: GameBloc.new,
seed: () => const GameState( seed: () => const GameState(
score: 0, totalScore: 10,
roundScore: 0,
multiplier: 6, multiplier: 6,
rounds: 3, rounds: 3,
bonusHistory: [], bonusHistory: [],
@ -160,15 +154,12 @@ void main() {
}, },
expect: () => [ expect: () => [
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 1) ..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', false), ..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 1) ..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', false), ..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>() isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 1) ..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', true), ..having((state) => state.isGameOver, 'isGameOver', true),
], ],

@ -8,14 +8,16 @@ void main() {
test('supports value equality', () { test('supports value equality', () {
expect( expect(
GameState( GameState(
score: 0, totalScore: 0,
roundScore: 0,
multiplier: 1, multiplier: 1,
rounds: 3, rounds: 3,
bonusHistory: const [], bonusHistory: const [],
), ),
equals( equals(
const GameState( const GameState(
score: 0, totalScore: 0,
roundScore: 0,
multiplier: 1, multiplier: 1,
rounds: 3, rounds: 3,
bonusHistory: [], bonusHistory: [],
@ -28,7 +30,8 @@ void main() {
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
const GameState( const GameState(
score: 0, totalScore: 0,
roundScore: 0,
multiplier: 1, multiplier: 1,
rounds: 3, rounds: 3,
bonusHistory: [], bonusHistory: [],
@ -40,11 +43,29 @@ void main() {
test( test(
'throws AssertionError ' 'throws AssertionError '
'when score is negative', 'when totalScore is negative',
() { () {
expect( expect(
() => GameState( () => GameState(
score: -1, totalScore: -1,
roundScore: 0,
multiplier: 1,
rounds: 3,
bonusHistory: const [],
),
throwsAssertionError,
);
},
);
test(
'throws AssertionError '
'when roundScore is negative',
() {
expect(
() => GameState(
totalScore: 0,
roundScore: -1,
multiplier: 1, multiplier: 1,
rounds: 3, rounds: 3,
bonusHistory: const [], bonusHistory: const [],
@ -60,7 +81,8 @@ void main() {
() { () {
expect( expect(
() => GameState( () => GameState(
score: 1, totalScore: 0,
roundScore: 1,
multiplier: 0, multiplier: 0,
rounds: 3, rounds: 3,
bonusHistory: const [], bonusHistory: const [],
@ -76,7 +98,8 @@ void main() {
() { () {
expect( expect(
() => GameState( () => GameState(
score: 1, totalScore: 0,
roundScore: 1,
multiplier: 1, multiplier: 1,
rounds: -1, rounds: -1,
bonusHistory: const [], bonusHistory: const [],
@ -91,7 +114,8 @@ void main() {
'is true ' 'is true '
'when no rounds are left', () { 'when no rounds are left', () {
const gameState = GameState( const gameState = GameState(
score: 0, totalScore: 0,
roundScore: 0,
multiplier: 1, multiplier: 1,
rounds: 0, rounds: 0,
bonusHistory: [], bonusHistory: [],
@ -103,7 +127,8 @@ void main() {
'is false ' 'is false '
'when one 1 round left', () { 'when one 1 round left', () {
const gameState = GameState( const gameState = GameState(
score: 0, totalScore: 0,
roundScore: 0,
multiplier: 1, multiplier: 1,
rounds: 1, rounds: 1,
bonusHistory: [], bonusHistory: [],
@ -115,16 +140,17 @@ void main() {
group('copyWith', () { group('copyWith', () {
test( test(
'throws AssertionError ' 'throws AssertionError '
'when scored is decreased', 'when totalScore is decreased',
() { () {
const gameState = GameState( const gameState = GameState(
score: 2, totalScore: 2,
roundScore: 2,
multiplier: 1, multiplier: 1,
rounds: 3, rounds: 3,
bonusHistory: [], bonusHistory: [],
); );
expect( expect(
() => gameState.copyWith(score: gameState.score - 1), () => gameState.copyWith(totalScore: gameState.totalScore - 1),
throwsAssertionError, throwsAssertionError,
); );
}, },
@ -135,7 +161,8 @@ void main() {
'when no argument specified', 'when no argument specified',
() { () {
const gameState = GameState( const gameState = GameState(
score: 2, totalScore: 0,
roundScore: 2,
multiplier: 1, multiplier: 1,
rounds: 3, rounds: 3,
bonusHistory: [], bonusHistory: [],
@ -152,13 +179,15 @@ void main() {
'when all arguments specified', 'when all arguments specified',
() { () {
const gameState = GameState( const gameState = GameState(
score: 2, totalScore: 0,
roundScore: 2,
multiplier: 1, multiplier: 1,
rounds: 3, rounds: 3,
bonusHistory: [], bonusHistory: [],
); );
final otherGameState = GameState( final otherGameState = GameState(
score: gameState.score + 1, totalScore: gameState.totalScore + 1,
roundScore: gameState.roundScore + 1,
multiplier: gameState.multiplier + 1, multiplier: gameState.multiplier + 1,
rounds: gameState.rounds + 1, rounds: gameState.rounds + 1,
bonusHistory: const [GameBonus.googleWord], bonusHistory: const [GameBonus.googleWord],
@ -167,7 +196,8 @@ void main() {
expect( expect(
gameState.copyWith( gameState.copyWith(
score: otherGameState.score, totalScore: otherGameState.totalScore,
roundScore: otherGameState.roundScore,
multiplier: otherGameState.multiplier, multiplier: otherGameState.multiplier,
rounds: otherGameState.rounds, rounds: otherGameState.rounds,
bonusHistory: otherGameState.bonusHistory, bonusHistory: otherGameState.bonusHistory,

@ -2,6 +2,7 @@
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/behaviors/bumper_noisy_behavior.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -33,11 +34,12 @@ void main() {
Assets.images.android.bumper.cow.lit.keyName, Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName, Assets.images.android.bumper.cow.dimmed.keyName,
]; ];
final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('AndroidAcres', () { group('AndroidAcres', () {
final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
flameTester.test('loads correctly', (game) async { flameTester.test('loads correctly', (game) async {
final component = AndroidAcres(); final component = AndroidAcres();
await game.ensureAdd(component); await game.ensureAdd(component);
@ -99,6 +101,20 @@ void main() {
); );
}, },
); );
flameTester.test(
'three AndroidBumpers with BumperNoisyBehavior',
(game) async {
await game.ensureAdd(AndroidAcres());
final bumpers = game.descendants().whereType<AndroidBumper>();
for (final bumper in bumpers) {
expect(
bumper.firstChild<BumperNoisyBehavior>(),
isNotNull,
);
}
},
);
}); });
flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async { flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async {

@ -0,0 +1,152 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.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/behaviors.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 'package:pinball_flame/pinball_flame.dart';
import '../../../../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
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;
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,
);
flameBlocTester.testGameWidget(
'when hits are multiples of 10 times adds a ScoringBehavior',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState(hits: 9),
);
final behavior = RampBonusBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
expect(scores.length, 1);
},
);
flameBlocTester.testGameWidget(
"when hits are not multiple of 10 times doesn't add any ScoringBehavior",
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState.initial(),
);
final behavior = RampBonusBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
expect(scores.length, 0);
},
);
flameBlocTester.testGameWidget(
'closes subscription when removed',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final subscription = _MockStreamSubscription();
when(subscription.cancel).thenAnswer((_) async {});
final behavior = RampBonusBehavior.test(
points: shotPoints,
subscription: subscription,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
parent.remove(behavior);
await game.ready();
verify(subscription.cancel).called(1);
},
);
});
}

@ -0,0 +1,156 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.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/behaviors.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 'package:pinball_flame/pinball_flame.dart';
import '../../../../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
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;
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,
);
flameBlocTester.testGameWidget(
'when hits are not multiple of 10 times '
'increases multiplier and adds a ScoringBehavior',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState.initial(),
);
final behavior = RampShotBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
verify(() => gameBloc.add(MultiplierIncreased())).called(1);
expect(scores.length, 1);
},
);
flameBlocTester.testGameWidget(
'when hits multiple of 10 times '
"doesn't increase multiplier, neither ScoringBehavior",
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState(hits: 9),
);
final behavior = RampShotBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10));
final scores = game.children.whereType<ScoringBehavior>();
await game.ready();
verifyNever(() => gameBloc.add(MultiplierIncreased()));
expect(scores.length, 0);
},
);
flameBlocTester.testGameWidget(
'closes subscription when removed',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final subscription = _MockStreamSubscription();
when(subscription.cancel).thenAnswer((_) async {});
final behavior = RampShotBehavior.test(
points: shotPoints,
subscription: subscription,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
parent.remove(behavior);
await game.ready();
verify(subscription.cancel).called(1);
},
);
});
}

@ -0,0 +1,98 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/backbox/displays/initials_input_display.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../helpers/helpers.dart';
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get score => '';
@override
String get name => '';
@override
String get enterInitials => '';
@override
String get arrows => '';
@override
String get andPress => '';
@override
String get enterReturn => '';
@override
String get toSubmit => '';
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final characterIconPath = theme.Assets.images.dash.leaderboardIcon.keyName;
final assets = [
characterIconPath,
Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(
assets: assets,
l10n: _MockAppLocalizations(),
),
);
group('Backbox', () {
flameTester.test(
'loads correctly',
(game) async {
final backbox = Backbox();
await game.ensureAdd(backbox);
expect(game.children, contains(backbox));
},
);
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
game.camera
..followVector2(Vector2(0, -130))
..zoom = 6;
await game.ensureAdd(Backbox());
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<EmptyPinballTestGame>(),
matchesGoldenFile('../golden/backbox.png'),
);
},
);
flameTester.test(
'initialsInput adds InitialsInputDisplay',
(game) async {
final backbox = Backbox();
await game.ensureAdd(backbox);
await backbox.initialsInput(
score: 0,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ready();
expect(backbox.firstChild<InitialsInputDisplay>(), isNotNull);
},
);
});
}

@ -0,0 +1,189 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/backbox/displays/initials_input_display.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get score => '';
@override
String get name => '';
@override
String get enterInitials => '';
@override
String get arrows => '';
@override
String get andPress => '';
@override
String get enterReturn => '';
@override
String get toSubmit => '';
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final characterIconPath = theme.Assets.images.dash.leaderboardIcon.keyName;
final assets = [
characterIconPath,
Assets.images.backbox.displayDivider.keyName,
];
final flameTester = FlameTester(
() => EmptyKeyboardPinballTestGame(
assets: assets,
l10n: _MockAppLocalizations(),
),
);
group('InitialsInputDisplay', () {
flameTester.test(
'loads correctly',
(game) async {
final initialsInputDisplay = InitialsInputDisplay(
score: 0,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(initialsInputDisplay);
expect(game.children, contains(initialsInputDisplay));
},
);
flameTester.testGameWidget(
'can change the initials',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final initialsInputDisplay = InitialsInputDisplay(
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(initialsInputDisplay);
// Focus is already on the first letter
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// Move to the next an press up again
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// One more time
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// Back to the previous and increase one more
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final initialsInputDisplay =
game.descendants().whereType<InitialsInputDisplay>().single;
expect(initialsInputDisplay.initials, equals('BCB'));
},
);
String? submitedInitials;
flameTester.testGameWidget(
'submits the initials',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final initialsInputDisplay = InitialsInputDisplay(
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (value) {
submitedInitials = value;
},
);
await game.ensureAdd(initialsInputDisplay);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
},
verify: (game, tester) async {
expect(submitedInitials, equals('AAA'));
},
);
group('BackboardLetterPrompt', () {
flameTester.testGameWidget(
'cycles the char up and down when it has focus',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
InitialsLetterPrompt(hasFocus: true, position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<InitialsLetterPrompt>();
expect(prompt?.char, equals('C'));
},
);
flameTester.testGameWidget(
"does nothing when it doesn't have focus",
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
InitialsLetterPrompt(position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<InitialsLetterPrompt>();
expect(prompt?.char, equals('A'));
},
);
flameTester.testGameWidget(
'blinks the prompt when it has the focus',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
InitialsLetterPrompt(position: Vector2.zero(), hasFocus: true),
);
},
verify: (game, tester) async {
final underscore =
game.descendants().whereType<ShapeComponent>().first;
expect(underscore.paint.color, Colors.white);
game.update(2);
expect(underscore.paint.color, Colors.transparent);
},
);
});
});
}

@ -24,15 +24,18 @@ void main() {
test('correctly calculates the zooms', () async { test('correctly calculates the zooms', () async {
expect(controller.gameFocus.zoom.toInt(), equals(12)); expect(controller.gameFocus.zoom.toInt(), equals(12));
expect(controller.backboardFocus.zoom.toInt(), equals(11)); expect(controller.waitingBackboxFocus.zoom.toInt(), equals(11));
}); });
test('correctly sets the initial zoom and position', () async { test('correctly sets the initial zoom and position', () async {
expect(game.camera.zoom, equals(controller.backboardFocus.zoom)); expect(game.camera.zoom, equals(controller.waitingBackboxFocus.zoom));
expect(game.camera.follow, equals(controller.backboardFocus.position)); expect(
game.camera.follow,
equals(controller.waitingBackboxFocus.position),
);
}); });
group('focusOnBoard', () { group('focusOnGame', () {
test('changes the zoom', () async { test('changes the zoom', () async {
controller.focusOnGame(); controller.focusOnGame();
@ -53,22 +56,22 @@ void main() {
await future; await future;
expect(game.camera.position, Vector2(-4, -108.8)); expect(game.camera.position, Vector2(-4, -120));
}); });
}); });
group('focusOnBackboard', () { group('focusOnWaitingBackbox', () {
test('changes the zoom', () async { test('changes the zoom', () async {
controller.focusOnBackboard(); controller.focusOnWaitingBackbox();
await game.ready(); await game.ready();
final zoom = game.firstChild<CameraZoom>(); final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull); expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.backboardFocus.zoom)); expect(zoom?.value, equals(controller.waitingBackboxFocus.zoom));
}); });
test('moves the camera after the zoom is completed', () async { test('moves the camera after the zoom is completed', () async {
controller.focusOnBackboard(); controller.focusOnWaitingBackbox();
await game.ready(); await game.ready();
final cameraZoom = game.firstChild<CameraZoom>()!; final cameraZoom = game.firstChild<CameraZoom>()!;
final future = cameraZoom.completed; final future = cameraZoom.completed;
@ -78,7 +81,32 @@ void main() {
await future; await future;
expect(game.camera.position, Vector2(-4.5, -109.8)); expect(game.camera.position, Vector2(-4.5, -121));
});
});
group('focusOnGameOverBackbox', () {
test('changes the zoom', () async {
controller.focusOnGameOverBackbox();
await game.ready();
final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.gameOverBackboxFocus.zoom));
});
test('moves the camera after the zoom is completed', () async {
controller.focusOnGameOverBackbox();
await game.ready();
final cameraZoom = game.firstChild<CameraZoom>()!;
final future = cameraZoom.completed;
game.update(10);
game.update(0); // Ensure that the component was removed
await future;
expect(game.camera.position, Vector2(-2.5, -117));
}); });
}); });
}); });

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -99,6 +100,7 @@ void main() {
group('turboCharge', () { group('turboCharge', () {
setUpAll(() { setUpAll(() {
registerFallbackValue(Vector2.zero()); registerFallbackValue(Vector2.zero());
registerFallbackValue(Component());
}); });
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
@ -124,7 +126,7 @@ void main() {
final controller = _WrappedBallController(ball, gameRef); final controller = _WrappedBallController(ball, gameRef);
when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc); when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc);
when(() => ball.controller).thenReturn(controller); when(() => ball.controller).thenReturn(controller);
when(() => ball.boost(any())).thenAnswer((_) async {}); when(() => ball.add(any())).thenAnswer((_) async {});
await controller.turboCharge(); await controller.turboCharge();
@ -140,29 +142,13 @@ void main() {
final controller = _WrappedBallController(ball, gameRef); final controller = _WrappedBallController(ball, gameRef);
when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc); when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc);
when(() => ball.controller).thenReturn(controller); when(() => ball.controller).thenReturn(controller);
when(() => ball.boost(any())).thenAnswer((_) async {}); when(() => ball.add(any())).thenAnswer((_) async {});
await controller.turboCharge(); await controller.turboCharge();
verify(ball.resume).called(1); verify(ball.resume).called(1);
}, },
); );
flameBlocTester.test(
'boosts the ball',
(game) async {
final gameRef = _MockPinballGame();
final ball = _MockControlledBall();
final controller = _WrappedBallController(ball, gameRef);
when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc);
when(() => ball.controller).thenReturn(controller);
when(() => ball.boost(any())).thenAnswer((_) async {});
await controller.turboCharge();
verify(() => ball.boost(any())).called(1);
},
);
}); });
}); });
} }

@ -27,7 +27,8 @@ void main() {
blocBuilder: () { blocBuilder: () {
final bloc = _MockGameBloc(); final bloc = _MockGameBloc();
const state = GameState( const state = GameState(
score: 0, totalScore: 0,
roundScore: 0,
multiplier: 1, multiplier: 1,
rounds: 0, rounds: 0,
bonusHistory: [], bonusHistory: [],

@ -22,7 +22,8 @@ void main() {
blocBuilder: () { blocBuilder: () {
final bloc = _MockGameBloc(); final bloc = _MockGameBloc();
const state = GameState( const state = GameState(
score: 0, totalScore: 0,
roundScore: 0,
multiplier: 1, multiplier: 1,
rounds: 0, rounds: 0,
bonusHistory: [], bonusHistory: [],

@ -2,6 +2,7 @@
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart'; import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -67,13 +68,13 @@ void main() {
group('adds', () { group('adds', () {
flameTester.test( flameTester.test(
'ScoringBehavior to ChromeDino', 'ScoringContactBehavior to ChromeDino',
(game) async { (game) async {
await game.ensureAdd(DinoDesert()); await game.ensureAdd(DinoDesert());
final chromeDino = game.descendants().whereType<ChromeDino>().single; final chromeDino = game.descendants().whereType<ChromeDino>().single;
expect( expect(
chromeDino.firstChild<ScoringBehavior>(), chromeDino.firstChild<ScoringContactBehavior>(),
isNotNull, isNotNull,
); );
}, },

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

Loading…
Cancel
Save