Merge branch 'main' into fix/flipper-movement

pull/175/head
Allison Ryan 3 years ago committed by GitHub
commit 8e37e6d8de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,5 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:math' as math;
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@ -9,19 +9,41 @@ part 'game_state.dart';
class GameBloc extends Bloc<GameEvent, GameState> { class GameBloc extends Bloc<GameEvent, GameState> {
GameBloc() : super(const GameState.initial()) { GameBloc() : super(const GameState.initial()) {
on<BallLost>(_onBallLost); on<RoundLost>(_onRoundLost);
on<Scored>(_onScored); on<Scored>(_onScored);
on<MultiplierIncreased>(_onIncreasedMultiplier);
on<BonusActivated>(_onBonusActivated); on<BonusActivated>(_onBonusActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated); on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
} }
void _onBallLost(BallLost event, Emitter emit) { void _onRoundLost(RoundLost event, Emitter emit) {
emit(state.copyWith(balls: state.balls - 1)); final score = state.score * state.multiplier;
final roundsLeft = math.max(state.rounds - 1, 0);
emit(
state.copyWith(
score: score,
multiplier: 1,
rounds: roundsLeft,
),
);
} }
void _onScored(Scored event, Emitter emit) { void _onScored(Scored event, Emitter emit) {
if (!state.isGameOver) { if (!state.isGameOver) {
emit(state.copyWith(score: state.score + event.points)); emit(
state.copyWith(score: state.score + event.points),
);
}
}
void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) {
if (!state.isGameOver) {
emit(
state.copyWith(
multiplier: math.min(state.multiplier + 1, 6),
),
);
} }
} }

@ -7,12 +7,12 @@ abstract class GameEvent extends Equatable {
const GameEvent(); const GameEvent();
} }
/// {@template ball_lost_game_event} /// {@template round_lost_game_event}
/// Event added when a user drops a ball off the screen. /// Event added when a user drops all balls off the screen and loses a round.
/// {@endtemplate} /// {@endtemplate}
class BallLost extends GameEvent { class RoundLost extends GameEvent {
/// {@macro ball_lost_game_event} /// {@macro round_lost_game_event}
const BallLost(); const RoundLost();
@override @override
List<Object?> get props => []; List<Object?> get props => [];
@ -48,3 +48,14 @@ class SparkyTurboChargeActivated extends GameEvent {
@override @override
List<Object?> get props => []; List<Object?> get props => [];
} }
/// {@template multiplier_increased_game_event}
/// Added when a multiplier is gained.
/// {@endtemplate}
class MultiplierIncreased extends GameEvent {
/// {@macro multiplier_increased_game_event}
const MultiplierIncreased();
@override
List<Object?> get props => [];
}

@ -27,34 +27,42 @@ class GameState extends Equatable {
/// {@macro game_state} /// {@macro game_state}
const GameState({ const GameState({
required this.score, required this.score,
required this.balls, required this.multiplier,
required this.rounds,
required this.bonusHistory, required this.bonusHistory,
}) : assert(score >= 0, "Score can't be negative"), }) : assert(score >= 0, "Score can't be negative"),
assert(balls >= 0, "Number of balls can't be negative"); assert(multiplier > 0, 'Multiplier must be greater than zero'),
assert(rounds >= 0, "Number of rounds can't be negative");
const GameState.initial() const GameState.initial()
: score = 0, : score = 0,
balls = 3, multiplier = 1,
rounds = 3,
bonusHistory = const []; bonusHistory = const [];
/// The current score of the game. /// The current score of the game.
final int score; final int score;
/// The number of balls left in the game. /// The current multiplier for the score.
final int multiplier;
/// The number of rounds left in the game.
/// ///
/// When the number of balls is 0, the game is over. /// When the number of rounds is 0, the game is over.
final int balls; final int rounds;
/// Holds the history of all the [GameBonus]es earned by the player during a /// Holds the history of all the [GameBonus]es earned by the player during a
/// PinballGame. /// PinballGame.
final List<GameBonus> bonusHistory; final List<GameBonus> bonusHistory;
/// Determines when the game is over. /// Determines when the game is over.
bool get isGameOver => balls == 0; bool get isGameOver => rounds == 0;
GameState copyWith({ GameState copyWith({
int? score, int? score,
int? multiplier,
int? balls, int? balls,
int? rounds,
List<GameBonus>? bonusHistory, List<GameBonus>? bonusHistory,
}) { }) {
assert( assert(
@ -64,7 +72,8 @@ class GameState extends Equatable {
return GameState( return GameState(
score: score ?? this.score, score: score ?? this.score,
balls: balls ?? this.balls, multiplier: multiplier ?? this.multiplier,
rounds: rounds ?? this.rounds,
bonusHistory: bonusHistory ?? this.bonusHistory, bonusHistory: bonusHistory ?? this.bonusHistory,
); );
} }
@ -72,7 +81,8 @@ class GameState extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
score, score,
balls, multiplier,
rounds,
bonusHistory, bonusHistory,
]; ];
} }

@ -5,25 +5,30 @@ import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template alien_zone} /// {@template android_acres}
/// Area positioned below [Spaceship] where the [Ball] /// Area positioned on the left side of the board containing the [Spaceship],
/// can bounce off [AlienBumper]s. /// [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s.
/// {@endtemplate} /// {@endtemplate}
class AlienZone extends Blueprint { class AndroidAcres extends Blueprint {
/// {@macro alien_zone} /// {@macro android_acres}
AlienZone() AndroidAcres()
: super( : super(
components: [ components: [
AlienBumper.a( AndroidBumper.a(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20),
], ],
)..initialPosition = Vector2(-32.52, -9.1), )..initialPosition = Vector2(-32.52, -9.1),
AlienBumper.b( AndroidBumper.b(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20),
], ],
)..initialPosition = Vector2(-22.89, -17.35), )..initialPosition = Vector2(-22.89, -17.35),
], ],
blueprints: [
SpaceshipRamp(),
Spaceship(position: Vector2(-26.5, -28.5)),
SpaceshipRail(),
],
); );
} }

@ -18,17 +18,8 @@ class Board extends Component {
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
// TODO(alestiago): adjust positioning to real design.
// TODO(alestiago): add dino in pinball game.
final dino = ChromeDino()
..initialPosition = Vector2(
BoardDimensions.bounds.center.dx + 25,
BoardDimensions.bounds.center.dy - 10,
);
await addAll([ await addAll([
bottomGroup, bottomGroup,
dino,
flutterForest, flutterForest,
]); ]);
} }

@ -1,4 +1,4 @@
export 'alien_zone.dart'; export 'android_acres.dart';
export 'board.dart'; export 'board.dart';
export 'camera_controller.dart'; export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';

@ -1,5 +1,4 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/forge2d_game.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_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -8,12 +7,12 @@ import 'package:pinball_theme/pinball_theme.dart';
/// {@template controlled_ball} /// {@template controlled_ball}
/// A [Ball] with a [BallController] attached. /// A [Ball] with a [BallController] attached.
///
/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is
/// not over, a new [Ball] will be spawned.
/// {@endtemplate} /// {@endtemplate}
class ControlledBall extends Ball with Controls<BallController> { class ControlledBall extends Ball with Controls<BallController> {
/// A [Ball] that launches from the [Plunger]. /// A [Ball] that launches from the [Plunger].
///
/// When a launched [Ball] is lost, it will decrease the [GameState.balls]
/// count, and a new [Ball] is spawned.
ControlledBall.launch({ ControlledBall.launch({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
}) : super(baseColor: characterTheme.ballColor) { }) : super(baseColor: characterTheme.ballColor) {
@ -24,8 +23,6 @@ class ControlledBall extends Ball with Controls<BallController> {
/// {@template bonus_ball} /// {@template bonus_ball}
/// {@macro controlled_ball} /// {@macro controlled_ball}
///
/// When a bonus [Ball] is lost, the [GameState.balls] doesn't change.
/// {@endtemplate} /// {@endtemplate}
ControlledBall.bonus({ ControlledBall.bonus({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
@ -36,7 +33,7 @@ class ControlledBall extends Ball with Controls<BallController> {
/// [Ball] used in [DebugPinballGame]. /// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = DebugBallController(this); controller = BallController(this);
priority = RenderPriority.ballOnBoard; priority = RenderPriority.ballOnBoard;
} }
} }
@ -74,15 +71,9 @@ class BallController extends ComponentController<Ball>
@override @override
void onRemove() { void onRemove() {
super.onRemove(); super.onRemove();
gameRef.read<GameBloc>().add(const BallLost()); final noBallsLeft = gameRef.descendants().whereType<Ball>().isEmpty;
if (noBallsLeft) {
gameRef.read<GameBloc>().add(const RoundLost());
}
} }
} }
/// {@macro ball_controller}
class DebugBallController extends BallController {
/// {@macro ball_controller}
DebugBallController(Ball<Forge2DGame> component) : super(component);
@override
void onRemove() {}
}

@ -34,8 +34,10 @@ extension PinballGameAssetsX on PinballGame {
images.load( images.load(
components.Assets.images.launchRamp.backgroundRailing.keyName, components.Assets.images.launchRamp.backgroundRailing.keyName,
), ),
images.load(components.Assets.images.dino.dinoLandTop.keyName), images.load(components.Assets.images.dino.bottomWall.keyName),
images.load(components.Assets.images.dino.dinoLandBottom.keyName), images.load(components.Assets.images.dino.topWall.keyName),
images.load(components.Assets.images.dino.animatronic.head.keyName),
images.load(components.Assets.images.dino.animatronic.mouth.keyName),
images.load(components.Assets.images.dash.animatronic.keyName), images.load(components.Assets.images.dash.animatronic.keyName),
images.load(components.Assets.images.dash.bumper.a.active.keyName), images.load(components.Assets.images.dash.bumper.a.active.keyName),
images.load(components.Assets.images.dash.bumper.a.inactive.keyName), images.load(components.Assets.images.dash.bumper.a.inactive.keyName),
@ -76,13 +78,11 @@ extension PinballGameAssetsX on PinballGame {
components.Assets.images.spaceship.ramp.arrow.active5.keyName, components.Assets.images.spaceship.ramp.arrow.active5.keyName,
), ),
images.load(components.Assets.images.spaceship.rail.main.keyName), images.load(components.Assets.images.spaceship.rail.main.keyName),
images.load(components.Assets.images.spaceship.rail.foreground.keyName), images.load(components.Assets.images.spaceship.rail.exit.keyName),
images.load(components.Assets.images.alienBumper.a.active.keyName), images.load(components.Assets.images.androidBumper.a.lit.keyName),
images.load(components.Assets.images.alienBumper.a.inactive.keyName), images.load(components.Assets.images.androidBumper.a.dimmed.keyName),
images.load(components.Assets.images.alienBumper.b.active.keyName), images.load(components.Assets.images.androidBumper.b.lit.keyName),
images.load(components.Assets.images.alienBumper.b.inactive.keyName), images.load(components.Assets.images.androidBumper.b.dimmed.keyName),
images.load(components.Assets.images.chromeDino.mouth.keyName),
images.load(components.Assets.images.chromeDino.head.keyName),
images.load(components.Assets.images.sparky.computer.top.keyName), images.load(components.Assets.images.sparky.computer.top.keyName),
images.load(components.Assets.images.sparky.computer.base.keyName), images.load(components.Assets.images.sparky.computer.base.keyName),
images.load(components.Assets.images.sparky.animatronic.keyName), images.load(components.Assets.images.sparky.animatronic.keyName),

@ -53,20 +53,11 @@ class PinballGame extends Forge2DGame
final launcher = Launcher(); final launcher = Launcher();
unawaited(addFromBlueprint(launcher)); unawaited(addFromBlueprint(launcher));
unawaited(add(Board())); unawaited(add(Board()));
await addFromBlueprint(AlienZone());
await addFromBlueprint(SparkyFireZone()); await addFromBlueprint(SparkyFireZone());
await addFromBlueprint(AndroidAcres());
unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls())); unawaited(addFromBlueprint(DinoWalls()));
unawaited(addFromBlueprint(SpaceshipRamp())); await add(ChromeDino()..initialPosition = Vector2(12.3, -6.9));
unawaited(
addFromBlueprint(
Spaceship(
position: Vector2(-26.5, -28.5),
),
),
);
unawaited(addFromBlueprint(SpaceshipRail()));
await add( await add(
GoogleWord( GoogleWord(
position: Vector2( position: Vector2(
@ -82,7 +73,7 @@ class PinballGame extends Forge2DGame
} }
class _GameBallsController extends ComponentController<PinballGame> class _GameBallsController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> { with BlocComponent<GameBloc, GameState> {
_GameBallsController(PinballGame game) : super(game); _GameBallsController(PinballGame game) : super(game);
late final Plunger _plunger; late final Plunger _plunger;
@ -90,9 +81,9 @@ class _GameBallsController extends ComponentController<PinballGame>
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty; final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
final canBallRespawn = newState.balls > 0; final notGameOver = !newState.isGameOver;
return noBallsLeft && canBallRespawn; return noBallsLeft && notGameOver;
} }
@override @override
@ -109,7 +100,7 @@ class _GameBallsController extends ComponentController<PinballGame>
void _spawnBall() { void _spawnBall() {
final ball = ControlledBall.launch( final ball = ControlledBall.launch(
characterTheme: gameRef.characterTheme, characterTheme: component.characterTheme,
)..initialPosition = Vector2( )..initialPosition = Vector2(
_plunger.body.position.x, _plunger.body.position.x,
_plunger.body.position.y - Ball.size.y, _plunger.body.position.y - Ball.size.y,
@ -170,20 +161,10 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector {
class _DebugGameBallsController extends _GameBallsController { class _DebugGameBallsController extends _GameBallsController {
_DebugGameBallsController(PinballGame game) : super(game); _DebugGameBallsController(PinballGame game) : super(game);
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component
.descendants()
.whereType<ControlledBall>()
.where((ball) => ball.controller is! DebugBallController)
.isEmpty;
final canBallRespawn = newState.balls > 0;
return noBallsLeft && canBallRespawn;
}
} }
// TODO(wolfenrain): investigate this CI failure.
// coverage:ignore-start
class _DebugInformation extends Component with HasGameRef<DebugPinballGame> { class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
_DebugInformation() : super(priority: RenderPriority.debugInfo); _DebugInformation() : super(priority: RenderPriority.debugInfo);
@ -215,3 +196,4 @@ class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
_debugTextPaint.render(canvas, debugText, position); _debugTextPaint.render(canvas, debugText, position);
} }
} }
// coverage:ignore-end

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

@ -14,9 +14,7 @@ class RoundCountDisplay extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
// TODO(arturplaczek): refactor when GameState handle balls and rounds and final rounds = context.select((GameBloc bloc) => bloc.state.rounds);
// select state.rounds property instead of state.ball
final balls = context.select((GameBloc bloc) => bloc.state.balls);
return Row( return Row(
children: [ children: [
@ -29,9 +27,9 @@ class RoundCountDisplay extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Row( Row(
children: [ children: [
RoundIndicator(isActive: balls >= 1), RoundIndicator(isActive: rounds >= 1),
RoundIndicator(isActive: balls >= 2), RoundIndicator(isActive: rounds >= 2),
RoundIndicator(isActive: balls >= 3), RoundIndicator(isActive: rounds >= 3),
], ],
), ),
], ],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 KiB

After

Width:  |  Height:  |  Size: 886 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 61 KiB

@ -10,14 +10,12 @@ import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
$AssetsImagesAlienBumperGen get alienBumper => $AssetsImagesAndroidBumperGen get androidBumper =>
const $AssetsImagesAlienBumperGen(); const $AssetsImagesAndroidBumperGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
$AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen();
$AssetsImagesChromeDinoGen get chromeDino =>
const $AssetsImagesChromeDinoGen();
$AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDashGen get dash => const $AssetsImagesDashGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
@ -33,11 +31,13 @@ class $AssetsImagesGen {
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
} }
class $AssetsImagesAlienBumperGen { class $AssetsImagesAndroidBumperGen {
const $AssetsImagesAlienBumperGen(); const $AssetsImagesAndroidBumperGen();
$AssetsImagesAlienBumperAGen get a => const $AssetsImagesAlienBumperAGen(); $AssetsImagesAndroidBumperAGen get a =>
$AssetsImagesAlienBumperBGen get b => const $AssetsImagesAlienBumperBGen(); const $AssetsImagesAndroidBumperAGen();
$AssetsImagesAndroidBumperBGen get b =>
const $AssetsImagesAndroidBumperBGen();
} }
class $AssetsImagesBackboardGen { class $AssetsImagesBackboardGen {
@ -95,18 +95,6 @@ class $AssetsImagesBoundaryGen {
const AssetGenImage('assets/images/boundary/outer.png'); const AssetGenImage('assets/images/boundary/outer.png');
} }
class $AssetsImagesChromeDinoGen {
const $AssetsImagesChromeDinoGen();
/// File path: assets/images/chrome_dino/head.png
AssetGenImage get head =>
const AssetGenImage('assets/images/chrome_dino/head.png');
/// File path: assets/images/chrome_dino/mouth.png
AssetGenImage get mouth =>
const AssetGenImage('assets/images/chrome_dino/mouth.png');
}
class $AssetsImagesDashGen { class $AssetsImagesDashGen {
const $AssetsImagesDashGen(); const $AssetsImagesDashGen();
@ -120,13 +108,16 @@ class $AssetsImagesDashGen {
class $AssetsImagesDinoGen { class $AssetsImagesDinoGen {
const $AssetsImagesDinoGen(); const $AssetsImagesDinoGen();
/// File path: assets/images/dino/dino-land-bottom.png $AssetsImagesDinoAnimatronicGen get animatronic =>
AssetGenImage get dinoLandBottom => const $AssetsImagesDinoAnimatronicGen();
const AssetGenImage('assets/images/dino/dino-land-bottom.png');
/// File path: assets/images/dino/bottom-wall.png
AssetGenImage get bottomWall =>
const AssetGenImage('assets/images/dino/bottom-wall.png');
/// File path: assets/images/dino/dino-land-top.png /// File path: assets/images/dino/top-wall.png
AssetGenImage get dinoLandTop => AssetGenImage get topWall =>
const AssetGenImage('assets/images/dino/dino-land-top.png'); const AssetGenImage('assets/images/dino/top-wall.png');
} }
class $AssetsImagesFlipperGen { class $AssetsImagesFlipperGen {
@ -269,28 +260,28 @@ class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyComputerGen(); const $AssetsImagesSparkyComputerGen();
} }
class $AssetsImagesAlienBumperAGen { class $AssetsImagesAndroidBumperAGen {
const $AssetsImagesAlienBumperAGen(); const $AssetsImagesAndroidBumperAGen();
/// File path: assets/images/alien_bumper/a/active.png /// File path: assets/images/android_bumper/a/dimmed.png
AssetGenImage get active => AssetGenImage get dimmed =>
const AssetGenImage('assets/images/alien_bumper/a/active.png'); const AssetGenImage('assets/images/android_bumper/a/dimmed.png');
/// File path: assets/images/alien_bumper/a/inactive.png /// File path: assets/images/android_bumper/a/lit.png
AssetGenImage get inactive => AssetGenImage get lit =>
const AssetGenImage('assets/images/alien_bumper/a/inactive.png'); const AssetGenImage('assets/images/android_bumper/a/lit.png');
} }
class $AssetsImagesAlienBumperBGen { class $AssetsImagesAndroidBumperBGen {
const $AssetsImagesAlienBumperBGen(); const $AssetsImagesAndroidBumperBGen();
/// File path: assets/images/alien_bumper/b/active.png /// File path: assets/images/android_bumper/b/dimmed.png
AssetGenImage get active => AssetGenImage get dimmed =>
const AssetGenImage('assets/images/alien_bumper/b/active.png'); const AssetGenImage('assets/images/android_bumper/b/dimmed.png');
/// File path: assets/images/alien_bumper/b/inactive.png /// File path: assets/images/android_bumper/b/lit.png
AssetGenImage get inactive => AssetGenImage get lit =>
const AssetGenImage('assets/images/alien_bumper/b/inactive.png'); const AssetGenImage('assets/images/android_bumper/b/lit.png');
} }
class $AssetsImagesDashBumperGen { class $AssetsImagesDashBumperGen {
@ -302,12 +293,24 @@ class $AssetsImagesDashBumperGen {
const $AssetsImagesDashBumperMainGen(); const $AssetsImagesDashBumperMainGen();
} }
class $AssetsImagesDinoAnimatronicGen {
const $AssetsImagesDinoAnimatronicGen();
/// File path: assets/images/dino/animatronic/head.png
AssetGenImage get head =>
const AssetGenImage('assets/images/dino/animatronic/head.png');
/// File path: assets/images/dino/animatronic/mouth.png
AssetGenImage get mouth =>
const AssetGenImage('assets/images/dino/animatronic/mouth.png');
}
class $AssetsImagesSpaceshipRailGen { class $AssetsImagesSpaceshipRailGen {
const $AssetsImagesSpaceshipRailGen(); const $AssetsImagesSpaceshipRailGen();
/// File path: assets/images/spaceship/rail/foreground.png /// File path: assets/images/spaceship/rail/exit.png
AssetGenImage get foreground => AssetGenImage get exit =>
const AssetGenImage('assets/images/spaceship/rail/foreground.png'); const AssetGenImage('assets/images/spaceship/rail/exit.png');
/// File path: assets/images/spaceship/rail/main.png /// File path: assets/images/spaceship/rail/main.png
AssetGenImage get main => AssetGenImage get main =>

@ -1,2 +0,0 @@
export 'alien_bumper_ball_contact_behavior.dart';
export 'alien_bumper_blinking_behavior.dart';

@ -1,17 +0,0 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'alien_bumper_state.dart';
class AlienBumperCubit extends Cubit<AlienBumperState> {
AlienBumperCubit() : super(AlienBumperState.active);
void onBallContacted() {
emit(AlienBumperState.inactive);
}
void onBlinked() {
emit(AlienBumperState.active);
}
}

@ -1,10 +0,0 @@
part of 'alien_bumper_cubit.dart';
/// Indicates the [AlienBumperCubit]'s current state.
enum AlienBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}

@ -4,71 +4,71 @@ 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_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/alien_bumper/behaviors/behaviors.dart'; import 'package:pinball_components/src/components/android_bumper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/alien_bumper_cubit.dart'; export 'cubit/android_bumper_cubit.dart';
/// {@template alien_bumper} /// {@template android_bumper}
/// Bumper for area under the [Spaceship]. /// Bumper for area under the [Spaceship].
/// {@endtemplate} /// {@endtemplate}
class AlienBumper extends BodyComponent with InitialPosition { class AndroidBumper extends BodyComponent with InitialPosition {
/// {@macro alien_bumper} /// {@macro android_bumper}
AlienBumper._({ AndroidBumper._({
required double majorRadius, required double majorRadius,
required double minorRadius, required double minorRadius,
required String onAssetPath, required String litAssetPath,
required String offAssetPath, required String dimmedAssetPath,
Iterable<Component>? children, Iterable<Component>? children,
required this.bloc, required this.bloc,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
_minorRadius = minorRadius, _minorRadius = minorRadius,
super( super(
priority: RenderPriority.alienBumper, priority: RenderPriority.androidBumper,
renderBody: false, renderBody: false,
children: [ children: [
AlienBumperBallContactBehavior(), AndroidBumperBallContactBehavior(),
AlienBumperBlinkingBehavior(), AndroidBumperBlinkingBehavior(),
_AlienBumperSpriteGroupComponent( _AndroidBumperSpriteGroupComponent(
offAssetPath: offAssetPath, dimmedAssetPath: dimmedAssetPath,
onAssetPath: onAssetPath, litAssetPath: litAssetPath,
state: bloc.state, state: bloc.state,
), ),
...?children, ...?children,
], ],
); );
/// {@macro alien_bumper} /// {@macro android_bumper}
AlienBumper.a({ AndroidBumper.a({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
majorRadius: 3.52, majorRadius: 3.52,
minorRadius: 2.97, minorRadius: 2.97,
onAssetPath: Assets.images.alienBumper.a.active.keyName, litAssetPath: Assets.images.androidBumper.a.lit.keyName,
offAssetPath: Assets.images.alienBumper.a.inactive.keyName, dimmedAssetPath: Assets.images.androidBumper.a.dimmed.keyName,
bloc: AlienBumperCubit(), bloc: AndroidBumperCubit(),
children: children, children: children,
); );
/// {@macro alien_bumper} /// {@macro android_bumper}
AlienBumper.b({ AndroidBumper.b({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
majorRadius: 3.19, majorRadius: 3.19,
minorRadius: 2.79, minorRadius: 2.79,
onAssetPath: Assets.images.alienBumper.b.active.keyName, litAssetPath: Assets.images.androidBumper.b.lit.keyName,
offAssetPath: Assets.images.alienBumper.b.inactive.keyName, dimmedAssetPath: Assets.images.androidBumper.b.dimmed.keyName,
bloc: AlienBumperCubit(), bloc: AndroidBumperCubit(),
children: children, children: children,
); );
/// Creates an [AlienBumper] without any children. /// Creates an [AndroidBumper] without any children.
/// ///
/// This can be used for testing [AlienBumper]'s behaviors in isolation. /// This can be used for testing [AndroidBumper]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged: // TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
AlienBumper.test({ AndroidBumper.test({
required this.bloc, required this.bloc,
}) : _majorRadius = 3.52, }) : _majorRadius = 3.52,
_minorRadius = 2.97; _minorRadius = 2.97;
@ -80,7 +80,7 @@ class AlienBumper extends BodyComponent with InitialPosition {
// TODO(alestiago): Consider refactoring once the following is merged: // TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final AlienBumperCubit bloc; final AndroidBumperCubit bloc;
@override @override
void onRemove() { void onRemove() {
@ -107,23 +107,23 @@ class AlienBumper extends BodyComponent with InitialPosition {
} }
} }
class _AlienBumperSpriteGroupComponent class _AndroidBumperSpriteGroupComponent
extends SpriteGroupComponent<AlienBumperState> extends SpriteGroupComponent<AndroidBumperState>
with HasGameRef, ParentIsA<AlienBumper> { with HasGameRef, ParentIsA<AndroidBumper> {
_AlienBumperSpriteGroupComponent({ _AndroidBumperSpriteGroupComponent({
required String onAssetPath, required String litAssetPath,
required String offAssetPath, required String dimmedAssetPath,
required AlienBumperState state, required AndroidBumperState state,
}) : _onAssetPath = onAssetPath, }) : _litAssetPath = litAssetPath,
_offAssetPath = offAssetPath, _dimmedAssetPath = dimmedAssetPath,
super( super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -0.1), position: Vector2(0, -0.1),
current: state, current: state,
); );
final String _onAssetPath; final String _litAssetPath;
final String _offAssetPath; final String _dimmedAssetPath;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -131,11 +131,11 @@ class _AlienBumperSpriteGroupComponent
parent.bloc.stream.listen((state) => current = state); parent.bloc.stream.listen((state) => current = state);
final sprites = { final sprites = {
AlienBumperState.active: Sprite( AndroidBumperState.lit: Sprite(
gameRef.images.fromCache(_onAssetPath), gameRef.images.fromCache(_litAssetPath),
), ),
AlienBumperState.inactive: AndroidBumperState.dimmed:
Sprite(gameRef.images.fromCache(_offAssetPath)), Sprite(gameRef.images.fromCache(_dimmedAssetPath)),
}; };
this.sprites = sprites; this.sprites = sprites;
size = sprites[current]!.originalSize / 10; size = sprites[current]!.originalSize / 10;

@ -4,7 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.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';
class AlienBumperBallContactBehavior extends ContactBehavior<AlienBumper> { class AndroidBumperBallContactBehavior extends ContactBehavior<AndroidBumper> {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);

@ -2,20 +2,20 @@ import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template alien_bumper_blinking_behavior} /// {@template android_bumper_blinking_behavior}
/// Makes a [AlienBumper] blink back to [AlienBumperState.active] when /// Makes an [AndroidBumper] blink back to [AndroidBumperState.lit] when
/// [AlienBumperState.inactive]. /// [AndroidBumperState.dimmed].
/// {@endtemplate} /// {@endtemplate}
class AlienBumperBlinkingBehavior extends TimerComponent class AndroidBumperBlinkingBehavior extends TimerComponent
with ParentIsA<AlienBumper> { with ParentIsA<AndroidBumper> {
/// {@macro alien_bumper_blinking_behavior} /// {@macro android_bumper_blinking_behavior}
AlienBumperBlinkingBehavior() : super(period: 0.05); AndroidBumperBlinkingBehavior() : super(period: 0.05);
void _onNewState(AlienBumperState state) { void _onNewState(AndroidBumperState state) {
switch (state) { switch (state) {
case AlienBumperState.active: case AndroidBumperState.lit:
break; break;
case AlienBumperState.inactive: case AndroidBumperState.dimmed:
timer timer
..reset() ..reset()
..start(); ..start();

@ -0,0 +1,2 @@
export 'android_bumper_ball_contact_behavior.dart';
export 'android_bumper_blinking_behavior.dart';

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'android_bumper_state.dart';
class AndroidBumperCubit extends Cubit<AndroidBumperState> {
AndroidBumperCubit() : super(AndroidBumperState.dimmed);
void onBallContacted() {
emit(AndroidBumperState.dimmed);
}
void onBlinked() {
emit(AndroidBumperState.lit);
}
}

@ -0,0 +1,8 @@
// ignore_for_file: public_member_api_docs
part of 'android_bumper_cubit.dart';
enum AndroidBumperState {
lit,
dimmed,
}

@ -67,7 +67,7 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
_BottomBoundarySpriteComponent() _BottomBoundarySpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-5.4, 55.6), position: Vector2(-5, 55.6),
); );
@override @override
@ -102,28 +102,59 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
Vector2(3.6, -70.2), Vector2(3.6, -70.2),
Vector2(-14.1, -70.2), Vector2(-14.1, -70.2),
); );
final topWallFixtureDef = FixtureDef(topWall);
final topLeftCurve = BezierCurveShape( final topLeftCurve = BezierCurveShape(
controlPoints: [ controlPoints: [
Vector2(-32.3, -57.2), topWall.vertex1,
Vector2(-31.5, -69.9), Vector2(-31.5, -69.9),
Vector2(-14.1, -70.2), Vector2(-32.3, -57.2),
], ],
); );
final topLeftCurveFixtureDef = FixtureDef(topLeftCurve);
final leftWall = EdgeShape() final topLeftWall = EdgeShape()
..set( ..set(
Vector2(-32.3, -57.2), topLeftCurve.vertices.last,
Vector2(-33.5, -44),
);
final upperLeftWallCurve = BezierCurveShape(
controlPoints: [
topLeftWall.vertex1,
Vector2(-33.9, -40.7),
Vector2(-32.5, -39),
],
);
final middleLeftWallCurve = BezierCurveShape(
controlPoints: [
upperLeftWallCurve.vertices.last,
Vector2(-23.2, -31.4),
Vector2(-33.9, -21.8),
],
);
final lowerLeftWallCurve = BezierCurveShape(
controlPoints: [
middleLeftWallCurve.vertices.last,
Vector2(-32.4, -17.6),
Vector2(-37.3, -11),
],
);
final bottomLeftWall = EdgeShape()
..set(
lowerLeftWallCurve.vertices.last,
Vector2(-43.9, 41.8), Vector2(-43.9, 41.8),
); );
final leftWallFixtureDef = FixtureDef(leftWall);
return [ return [
topWallFixtureDef, FixtureDef(topWall),
topLeftCurveFixtureDef, FixtureDef(topLeftCurve),
leftWallFixtureDef, FixtureDef(topLeftWall),
FixtureDef(upperLeftWallCurve),
FixtureDef(middleLeftWallCurve),
FixtureDef(lowerLeftWallCurve),
FixtureDef(bottomLeftWall),
]; ];
} }

@ -0,0 +1,25 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template bumping_behavior}
/// Makes any [BodyComponent] that contacts with [parent] bounce off.
/// {@endtemplate}
class BumpingBehavior extends ContactBehavior {
/// {@macro bumping_behavior}
BumpingBehavior({required double strength}) : _strength = strength;
/// Determines how strong the bump is.
final double _strength;
@override
void postSolve(Object other, Contact contact, ContactImpulse impulse) {
super.postSolve(other, contact, impulse);
if (other is! BodyComponent) return;
other.body.applyLinearImpulse(
contact.manifold.localPoint
..normalize()
..multiply(Vector2.all(other.body.mass * _strength)),
);
}
}

@ -1,31 +1,33 @@
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' hide Timer; import 'package:flame_forge2d/flame_forge2d.dart' hide Timer;
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template chrome_dino} /// {@template chrome_dino}
/// Dinosaur that gobbles up a [Ball], swivel his head around, and shoots it /// Dino that swivels back and forth, opening its mouth to eat a [Ball].
/// back out. ///
/// Upon eating a [Ball], the dino rotates and spits the [Ball] out in a
/// different direction.
/// {@endtemplate} /// {@endtemplate}
class ChromeDino extends BodyComponent with InitialPosition { class ChromeDino extends BodyComponent with InitialPosition {
/// {@macro chrome_dino} /// {@macro chrome_dino}
ChromeDino() ChromeDino()
: super( : super(
// TODO(alestiago): Remove once sprites are defined.
paint: Paint()..color = Colors.blue,
priority: RenderPriority.dino, priority: RenderPriority.dino,
renderBody: false,
); );
/// The size of the dinosaur mouth. /// The size of the dinosaur mouth.
static final size = Vector2(5, 2.5); static final size = Vector2(5.5, 5);
/// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc /// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc
/// motion. /// motion.
Future<_ChromeDinoJoint> _anchorToJoint() async { Future<_ChromeDinoJoint> _anchorToJoint() async {
final anchor = _ChromeDinoAnchor(); // TODO(allisonryan0002): try moving to anchor after new body is defined.
final anchor = _ChromeDinoAnchor()
..initialPosition = initialPosition + Vector2(9, -4);
await add(anchor); await add(anchor);
final jointDef = _ChromeDinoAnchorRevoluteJointDef( final jointDef = _ChromeDinoAnchorRevoluteJointDef(
@ -42,9 +44,11 @@ class ChromeDino extends BodyComponent with InitialPosition {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final joint = await _anchorToJoint(); final joint = await _anchorToJoint();
const framesInAnimation = 98;
const animationFPS = 1 / 24;
await add( await add(
TimerComponent( TimerComponent(
period: 1, period: (framesInAnimation / 2) * animationFPS,
onTick: joint._swivel, onTick: joint._swivel,
repeat: true, repeat: true,
), ),
@ -54,44 +58,17 @@ class ChromeDino extends BodyComponent with InitialPosition {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixtureDefs = <FixtureDef>[]; final fixtureDefs = <FixtureDef>[];
// TODO(alestiago): Subject to change when sprites are added. // TODO(allisonryan0002): Update this shape to better match sprite.
final box = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2); final box = PolygonShape()
final fixtureDef = FixtureDef( ..setAsBox(
box, size.x / 2,
density: 999, size.y / 2,
friction: 0.3, initialPosition + Vector2(-4, 2),
restitution: 0.1, -_ChromeDinoJoint._halfSweepingAngle,
isSensor: true, );
); final fixtureDef = FixtureDef(box, density: 1);
fixtureDefs.add(fixtureDef); fixtureDefs.add(fixtureDef);
// FIXME(alestiago): Investigate why adding these fixtures is considered as
// an invalid contact type.
// final upperEdge = EdgeShape()
// ..set(
// Vector2(-size.x / 2, -size.y / 2),
// Vector2(size.x / 2, -size.y / 2),
// );
// final upperEdgeDef = FixtureDef(upperEdge)..density = 0.5;
// fixtureDefs.add(upperEdgeDef);
// final lowerEdge = EdgeShape()
// ..set(
// Vector2(-size.x / 2, size.y / 2),
// Vector2(size.x / 2, size.y / 2),
// );
// final lowerEdgeDef = FixtureDef(lowerEdge)..density = 0.5;
// fixtureDefs.add(lowerEdgeDef);
// final rightEdge = EdgeShape()
// ..set(
// Vector2(size.x / 2, -size.y / 2),
// Vector2(size.x / 2, size.y / 2),
// );
// final rightEdgeDef = FixtureDef(rightEdge)..density = 0.5;
// fixtureDefs.add(rightEdgeDef);
return fixtureDefs; return fixtureDefs;
} }
@ -110,13 +87,18 @@ class ChromeDino extends BodyComponent with InitialPosition {
} }
} }
/// {@template flipper_anchor}
/// [JointAnchor] positioned at the end of a [ChromeDino].
/// {@endtemplate}
class _ChromeDinoAnchor extends JointAnchor { class _ChromeDinoAnchor extends JointAnchor {
/// {@macro flipper_anchor} _ChromeDinoAnchor();
_ChromeDinoAnchor() {
initialPosition = Vector2(ChromeDino.size.x / 2, 0); // TODO(allisonryan0002): if these aren't moved when fixing the rendering, see
// if the joint can be created in onMount to resolve render syncing.
@override
Future<void> onLoad() async {
await super.onLoad();
await addAll([
_ChromeDinoMouthSprite(),
_ChromeDinoHeadSprite(),
]);
} }
} }
@ -135,22 +117,86 @@ class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef {
chromeDino.body.position + anchor.body.position, chromeDino.body.position + anchor.body.position,
); );
enableLimit = true; enableLimit = true;
// TODO(alestiago): Apply design angle value. lowerAngle = -_ChromeDinoJoint._halfSweepingAngle;
const angle = math.pi / 3.5; upperAngle = _ChromeDinoJoint._halfSweepingAngle;
lowerAngle = -angle / 2;
upperAngle = angle / 2;
enableMotor = true; enableMotor = true;
// TODO(alestiago): Tune this values. maxMotorTorque = chromeDino.body.mass * 255;
maxMotorTorque = motorSpeed = chromeDino.body.mass * 30; motorSpeed = 2;
} }
} }
class _ChromeDinoJoint extends RevoluteJoint { class _ChromeDinoJoint extends RevoluteJoint {
_ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def); _ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def);
static const _halfSweepingAngle = 0.1143;
/// Sweeps the [ChromeDino] up and down repeatedly. /// Sweeps the [ChromeDino] up and down repeatedly.
void _swivel() { void _swivel() {
setMotorSpeed(-motorSpeed); setMotorSpeed(-motorSpeed);
} }
} }
class _ChromeDinoMouthSprite extends SpriteAnimationComponent with HasGameRef {
_ChromeDinoMouthSprite()
: super(
anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29),
angle: _ChromeDinoJoint._halfSweepingAngle,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final image = gameRef.images.fromCache(
Assets.images.dino.animatronic.mouth.keyName,
);
const amountPerRow = 11;
const amountPerColumn = 9;
final textureSize = Vector2(
image.width / amountPerRow,
image.height / amountPerColumn,
);
size = textureSize / 10;
final data = SpriteAnimationData.sequenced(
amount: (amountPerColumn * amountPerRow) - 1,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
);
animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45;
}
}
class _ChromeDinoHeadSprite extends SpriteAnimationComponent with HasGameRef {
_ChromeDinoHeadSprite()
: super(
anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29),
angle: _ChromeDinoJoint._halfSweepingAngle,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final image = gameRef.images.fromCache(
Assets.images.dino.animatronic.head.keyName,
);
const amountPerRow = 11;
const amountPerColumn = 9;
final textureSize = Vector2(
image.width / amountPerRow,
image.height / amountPerColumn,
);
size = textureSize / 10;
final data = SpriteAnimationData.sequenced(
amount: (amountPerColumn * amountPerRow) - 1,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
);
animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45;
}
}

@ -1,4 +1,4 @@
export 'alien_bumper/alien_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'backboard/backboard.dart'; export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';

@ -35,51 +35,46 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topStraightShape = EdgeShape() final topStraightShape = EdgeShape()
..set( ..set(
Vector2(28.65, -35.1), Vector2(28.65, -34.3),
Vector2(29.5, -35.1), Vector2(29.5, -34.3),
); );
final topStraightFixtureDef = FixtureDef(topStraightShape);
final topCurveShape = BezierCurveShape( final topCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
topStraightShape.vertex1, topStraightShape.vertex1,
Vector2(18.8, -27), Vector2(18.8, -26.2),
Vector2(26.6, -21), Vector2(26.6, -20.2),
], ],
); );
final topCurveFixtureDef = FixtureDef(topCurveShape);
final middleCurveShape = BezierCurveShape( final middleCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
topCurveShape.vertices.last, topCurveShape.vertices.last,
Vector2(27.8, -20.1), Vector2(27.8, -19.3),
Vector2(26.8, -19.5), Vector2(26.8, -18.7),
], ],
); );
final middleCurveFixtureDef = FixtureDef(middleCurveShape);
final bottomCurveShape = BezierCurveShape( final bottomCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
middleCurveShape.vertices.last, middleCurveShape.vertices.last,
Vector2(23, -15), Vector2(23, -14.2),
Vector2(27, -15), Vector2(27, -14.2),
], ],
); );
final bottomCurveFixtureDef = FixtureDef(bottomCurveShape);
final bottomStraightShape = EdgeShape() final bottomStraightShape = EdgeShape()
..set( ..set(
bottomCurveShape.vertices.last, bottomCurveShape.vertices.last,
Vector2(31, -14.5), Vector2(31, -13.7),
); );
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
return [ return [
topStraightFixtureDef, FixtureDef(topStraightShape),
topCurveFixtureDef, FixtureDef(topCurveShape),
middleCurveFixtureDef, FixtureDef(middleCurveShape),
bottomCurveFixtureDef, FixtureDef(bottomCurveShape),
bottomStraightFixtureDef, FixtureDef(bottomStraightShape),
]; ];
} }
@ -109,12 +104,12 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef {
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.dino.dinoLandTop.keyName, Assets.images.dino.topWall.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
position = Vector2(22.8, -38.9); position = Vector2(22.8, -38.1);
} }
} }
@ -131,17 +126,11 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
); );
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
const restitution = 1.0;
final topStraightShape = EdgeShape() final topStraightShape = EdgeShape()
..set( ..set(
Vector2(32.4, -8.8), Vector2(32.4, -8.8),
Vector2(25, -7.7), Vector2(25, -7.7),
); );
final topStraightFixtureDef = FixtureDef(
topStraightShape,
restitution: restitution,
);
final topLeftCurveShape = BezierCurveShape( final topLeftCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
@ -150,36 +139,24 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
Vector2(29.8, 13.8), Vector2(29.8, 13.8),
], ],
); );
final topLeftCurveFixtureDef = FixtureDef(
topLeftCurveShape,
restitution: restitution,
);
final bottomLeftStraightShape = EdgeShape() final bottomLeftStraightShape = EdgeShape()
..set( ..set(
topLeftCurveShape.vertices.last, topLeftCurveShape.vertices.last,
Vector2(31.9, 44.1), Vector2(31.9, 44.1),
); );
final bottomLeftStraightFixtureDef = FixtureDef(
bottomLeftStraightShape,
restitution: restitution,
);
final bottomStraightShape = EdgeShape() final bottomStraightShape = EdgeShape()
..set( ..set(
bottomLeftStraightShape.vertex2, bottomLeftStraightShape.vertex2,
Vector2(37.8, 44.1), Vector2(37.8, 44.1),
); );
final bottomStraightFixtureDef = FixtureDef(
bottomStraightShape,
restitution: restitution,
);
return [ return [
topStraightFixtureDef, FixtureDef(topStraightShape),
topLeftCurveFixtureDef, FixtureDef(topLeftCurveShape),
bottomLeftStraightFixtureDef, FixtureDef(bottomLeftStraightShape),
bottomStraightFixtureDef, FixtureDef(bottomStraightShape),
]; ];
} }
@ -203,11 +180,11 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef {
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.dino.dinoLandBottom.keyName, Assets.images.dino.bottomWall.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
position = Vector2(23.6, -9.5); position = Vector2(23.8, -9.5);
} }
} }

@ -24,7 +24,7 @@ abstract class RenderPriority {
static const int ballOnSpaceship = _above + spaceshipSaucer; static const int ballOnSpaceship = _above + spaceshipSaucer;
/// Render priority for the [Ball] while it's on the [SpaceshipRail]. /// Render priority for the [Ball] while it's on the [SpaceshipRail].
static const int ballOnSpaceshipRail = _below + spaceshipSaucer; static const int ballOnSpaceshipRail = _above + spaceshipRail;
/// Render priority for the [Ball] while it's on the [LaunchRamp]. /// Render priority for the [Ball] while it's on the [LaunchRamp].
static const int ballOnLaunchRamp = _above + launchRamp; static const int ballOnLaunchRamp = _above + launchRamp;
@ -83,13 +83,13 @@ abstract class RenderPriority {
static const int turboChargeFlame = _above + ballOnBoard; static const int turboChargeFlame = _above + ballOnBoard;
// Android Spaceship // Android Acres
static const int spaceshipRail = _above + bottomGroup; static const int spaceshipRail = _above + bottomGroup;
static const int spaceshipRailForeground = _above + spaceshipRail; static const int spaceshipRailExit = _above + ballOnSpaceshipRail;
static const int spaceshipSaucer = _above + spaceshipRail; static const int spaceshipSaucer = _above + ballOnSpaceshipRail;
static const int spaceshipSaucerWall = _above + spaceshipSaucer; static const int spaceshipSaucerWall = _above + spaceshipSaucer;
@ -106,7 +106,7 @@ abstract class RenderPriority {
static const int spaceshipRampBoardOpening = _below + ballOnBoard; static const int spaceshipRampBoardOpening = _below + ballOnBoard;
static const int alienBumper = _above + ballOnBoard; static const int androidBumper = _above + ballOnBoard;
// Score Text // Score Text

@ -2,88 +2,71 @@ 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:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_rail} /// {@template spaceship_rail}
/// A [Blueprint] for the spaceship drop tube. /// A [Blueprint] for the rail exiting the [Spaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRail extends Blueprint { class SpaceshipRail extends Blueprint {
/// {@macro spaceship_rail} /// {@macro spaceship_rail}
SpaceshipRail() SpaceshipRail()
: super( : super(
components: [ components: [
_SpaceshipRailRamp(), _SpaceshipRail(),
_SpaceshipRailExit(), _SpaceshipRailExit(),
_SpaceshipRailBase(radius: 0.55) _SpaceshipRailExitSpriteComponent()
..initialPosition = Vector2(-26.15, -18.65),
_SpaceshipRailBase(radius: 0.8)
..initialPosition = Vector2(-25.5, 12.9),
_SpaceshipRailForeground()
], ],
); );
} }
class _SpaceshipRailRamp extends BodyComponent with Layered { class _SpaceshipRail extends BodyComponent with Layered {
_SpaceshipRailRamp() _SpaceshipRail()
: super( : super(
priority: RenderPriority.spaceshipRail, priority: RenderPriority.spaceshipRail,
children: [_SpaceshipRailSpriteComponent()],
renderBody: false, renderBody: false,
children: [_SpaceshipRailRampSpriteComponent()],
) { ) {
layer = Layer.spaceshipExitRail; layer = Layer.spaceshipExitRail;
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[];
final topArcShape = ArcShape( final topArcShape = ArcShape(
center: Vector2(-35.5, -30.9), center: Vector2(-35.1, -30.9),
arcRadius: 2.5, arcRadius: 2.5,
angle: math.pi, angle: math.pi,
rotation: 0.2, rotation: 0.2,
); );
final topArcFixtureDef = FixtureDef(topArcShape);
fixturesDefs.add(topArcFixtureDef);
final topLeftCurveShape = BezierCurveShape( final topLeftCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
Vector2(-37.9, -30.4), Vector2(-37.6, -30.4),
Vector2(-38, -23.9), Vector2(-37.8, -23.9),
Vector2(-30.93, -18.2), Vector2(-30.93, -18.2),
], ],
); );
final topLeftCurveFixtureDef = FixtureDef(topLeftCurveShape);
fixturesDefs.add(topLeftCurveFixtureDef);
final middleLeftCurveShape = BezierCurveShape( final middleLeftCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
topLeftCurveShape.vertices.last, topLeftCurveShape.vertices.last,
Vector2(-22.6, -10.3), Vector2(-22.6, -10.3),
Vector2(-30, -0.2), Vector2(-29.5, -0.2),
], ],
); );
final middleLeftCurveFixtureDef = FixtureDef(middleLeftCurveShape);
fixturesDefs.add(middleLeftCurveFixtureDef);
final bottomLeftCurveShape = BezierCurveShape( final bottomLeftCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
middleLeftCurveShape.vertices.last, middleLeftCurveShape.vertices.last,
Vector2(-36, 8.6), Vector2(-35.6, 8.6),
Vector2(-32.04, 18.3), Vector2(-31.3, 18.3),
], ],
); );
final bottomLeftCurveFixtureDef = FixtureDef(bottomLeftCurveShape);
fixturesDefs.add(bottomLeftCurveFixtureDef);
final topRightStraightShape = EdgeShape() final topRightStraightShape = EdgeShape()
..set( ..set(
Vector2(-33, -31.3),
Vector2(-27.2, -21.3), Vector2(-27.2, -21.3),
Vector2(-33, -31.3),
); );
final topRightStraightFixtureDef = FixtureDef(topRightStraightShape);
fixturesDefs.add(topRightStraightFixtureDef);
final middleRightCurveShape = BezierCurveShape( final middleRightCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
@ -92,8 +75,6 @@ class _SpaceshipRailRamp extends BodyComponent with Layered {
Vector2(-25.29, 1.7), Vector2(-25.29, 1.7),
], ],
); );
final middleRightCurveFixtureDef = FixtureDef(middleRightCurveShape);
fixturesDefs.add(middleRightCurveFixtureDef);
final bottomRightCurveShape = BezierCurveShape( final bottomRightCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
@ -102,10 +83,16 @@ class _SpaceshipRailRamp extends BodyComponent with Layered {
Vector2(-26.8, 15.7), Vector2(-26.8, 15.7),
], ],
); );
final bottomRightCurveFixtureDef = FixtureDef(bottomRightCurveShape);
fixturesDefs.add(bottomRightCurveFixtureDef);
return fixturesDefs; return [
FixtureDef(topArcShape),
FixtureDef(topLeftCurveShape),
FixtureDef(middleLeftCurveShape),
FixtureDef(bottomLeftCurveShape),
FixtureDef(topRightStraightShape),
FixtureDef(middleRightCurveShape),
FixtureDef(bottomRightCurveShape),
];
} }
@override @override
@ -116,55 +103,47 @@ class _SpaceshipRailRamp extends BodyComponent with Layered {
} }
} }
class _SpaceshipRailRampSpriteComponent extends SpriteComponent class _SpaceshipRailSpriteComponent extends SpriteComponent with HasGameRef {
with HasGameRef { _SpaceshipRailSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-29.4, -5.7),
);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprite = await gameRef.loadSprite( final sprite = Sprite(
Assets.images.spaceship.rail.main.keyName, gameRef.images.fromCache(
Assets.images.spaceship.rail.main.keyName,
),
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-29.4, -5.7);
} }
} }
class _SpaceshipRailForeground extends SpriteComponent with HasGameRef { class _SpaceshipRailExitSpriteComponent extends SpriteComponent
_SpaceshipRailForeground() with HasGameRef {
: super(priority: RenderPriority.spaceshipRailForeground); _SpaceshipRailExitSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-28, 19.4),
priority: RenderPriority.spaceshipRailExit,
);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprite = await gameRef.loadSprite( final sprite = Sprite(
Assets.images.spaceship.rail.foreground.keyName, gameRef.images.fromCache(
Assets.images.spaceship.rail.exit.keyName,
),
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-28.5, 19.7);
}
}
/// Represents the ground bases of the [_SpaceshipRailRamp].
class _SpaceshipRailBase extends BodyComponent with InitialPosition {
_SpaceshipRailBase({required this.radius}) : super(renderBody: false);
final double radius;
@override
Body createBody() {
final shape = CircleShape()..radius = radius;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: initialPosition,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
} }

@ -50,6 +50,7 @@ flutter:
- assets/images/baseboard/ - assets/images/baseboard/
- assets/images/boundary/ - assets/images/boundary/
- assets/images/dino/ - assets/images/dino/
- assets/images/dino/animatronic/
- assets/images/flipper/ - assets/images/flipper/
- assets/images/launch_ramp/ - assets/images/launch_ramp/
- assets/images/dash/ - assets/images/dash/
@ -60,12 +61,11 @@ flutter:
- assets/images/spaceship/rail/ - assets/images/spaceship/rail/
- assets/images/spaceship/ramp/ - assets/images/spaceship/ramp/
- assets/images/spaceship/ramp/arrow/ - assets/images/spaceship/ramp/arrow/
- assets/images/chrome_dino/
- assets/images/kicker/ - assets/images/kicker/
- assets/images/plunger/ - assets/images/plunger/
- assets/images/slingshot/ - assets/images/slingshot/
- assets/images/alien_bumper/a/ - assets/images/android_bumper/a/
- assets/images/alien_bumper/b/ - assets/images/android_bumper/b/
- assets/images/sparky/ - assets/images/sparky/
- assets/images/sparky/computer/ - assets/images/sparky/computer/
- assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/a/

@ -23,7 +23,7 @@ void main() {
addPlungerStories(dashbook); addPlungerStories(dashbook);
addSlingshotStories(dashbook); addSlingshotStories(dashbook);
addSparkyBumperStories(dashbook); addSparkyBumperStories(dashbook);
addAlienZoneStories(dashbook); addAndroidAcresStories(dashbook);
addBoundariesStories(dashbook); addBoundariesStories(dashbook);
addGoogleWordStories(dashbook); addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook); addLaunchRampStories(dashbook);

@ -1,36 +0,0 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/alien_zone/alien_bumper_a_game.dart';
import 'package:sandbox/stories/alien_zone/alien_bumper_b_game.dart';
import 'package:sandbox/stories/alien_zone/spaceship_game.dart';
import 'package:sandbox/stories/alien_zone/spaceship_rail_game.dart';
import 'package:sandbox/stories/alien_zone/spaceship_ramp_game.dart';
void addAlienZoneStories(Dashbook dashbook) {
dashbook.storiesOf('Alien Zone')
..addGame(
title: 'Alien Bumper A',
description: AlienBumperAGame.description,
gameBuilder: (_) => AlienBumperAGame(),
)
..addGame(
title: 'Alien Bumper B',
description: AlienBumperBGame.description,
gameBuilder: (_) => AlienBumperBGame(),
)
..addGame(
title: 'Spaceship',
description: SpaceshipGame.description,
gameBuilder: (_) => SpaceshipGame(),
)
..addGame(
title: 'Spaceship Rail',
description: SpaceshipRailGame.description,
gameBuilder: (_) => SpaceshipRailGame(),
)
..addGame(
title: 'Spaceship Ramp',
description: SpaceshipRampGame.description,
gameBuilder: (_) => SpaceshipRampGame(),
);
}

@ -4,18 +4,18 @@ import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AlienBumperBGame extends BallGame { class AndroidBumperAGame extends BallGame {
AlienBumperBGame() AndroidBumperAGame()
: super( : super(
color: const Color(0xFF0000FF), color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.alienBumper.b.active.keyName, Assets.images.androidBumper.a.lit.keyName,
Assets.images.alienBumper.b.inactive.keyName, Assets.images.androidBumper.a.dimmed.keyName,
], ],
); );
static const description = ''' static const description = '''
Shows how a AlienBumperB is rendered. Shows how a AndroidBumperA is rendered.
- Activate the "trace" parameter to overlay the body. - Activate the "trace" parameter to overlay the body.
'''; ''';
@ -26,7 +26,7 @@ class AlienBumperBGame extends BallGame {
camera.followVector2(Vector2.zero()); camera.followVector2(Vector2.zero());
await add( await add(
AlienBumper.b()..priority = 1, AndroidBumper.a()..priority = 1,
); );
await traceAllBodies(); await traceAllBodies();

@ -4,18 +4,18 @@ import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AlienBumperAGame extends BallGame { class AndroidBumperBGame extends BallGame {
AlienBumperAGame() AndroidBumperBGame()
: super( : super(
color: const Color(0xFF0000FF), color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.alienBumper.a.active.keyName, Assets.images.androidBumper.b.lit.keyName,
Assets.images.alienBumper.a.inactive.keyName, Assets.images.androidBumper.b.dimmed.keyName,
], ],
); );
static const description = ''' static const description = '''
Shows how a AlienBumperA is rendered. Shows how a AndroidBumperB is rendered.
- Activate the "trace" parameter to overlay the body. - Activate the "trace" parameter to overlay the body.
'''; ''';
@ -26,7 +26,7 @@ class AlienBumperAGame extends BallGame {
camera.followVector2(Vector2.zero()); camera.followVector2(Vector2.zero());
await add( await add(
AlienBumper.a()..priority = 1, AndroidBumper.b()..priority = 1,
); );
await traceAllBodies(); await traceAllBodies();

@ -12,6 +12,10 @@ class SpaceshipRailGame extends BallGame {
color: Colors.blue, color: Colors.blue,
ballPriority: RenderPriority.ballOnSpaceshipRail, ballPriority: RenderPriority.ballOnSpaceshipRail,
ballLayer: Layer.spaceshipExitRail, ballLayer: Layer.spaceshipExitRail,
imagesFileNames: [
Assets.images.spaceship.rail.main.keyName,
Assets.images.spaceship.rail.exit.keyName,
],
); );
static const description = ''' static const description = '''

@ -0,0 +1,36 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/android_acres/android_bumper_a_game.dart';
import 'package:sandbox/stories/android_acres/android_bumper_b_game.dart';
import 'package:sandbox/stories/android_acres/spaceship_game.dart';
import 'package:sandbox/stories/android_acres/spaceship_rail_game.dart';
import 'package:sandbox/stories/android_acres/spaceship_ramp_game.dart';
void addAndroidAcresStories(Dashbook dashbook) {
dashbook.storiesOf('Android Acres')
..addGame(
title: 'Android Bumper A',
description: AndroidBumperAGame.description,
gameBuilder: (_) => AndroidBumperAGame(),
)
..addGame(
title: 'Android Bumper B',
description: AndroidBumperBGame.description,
gameBuilder: (_) => AndroidBumperBGame(),
)
..addGame(
title: 'Spaceship',
description: SpaceshipGame.description,
gameBuilder: (_) => SpaceshipGame(),
)
..addGame(
title: 'Spaceship Rail',
description: SpaceshipRailGame.description,
gameBuilder: (_) => SpaceshipRailGame(),
)
..addGame(
title: 'Spaceship Ramp',
description: SpaceshipRampGame.description,
gameBuilder: (_) => SpaceshipRampGame(),
);
}

@ -1,8 +1,22 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class ChromeDinoGame extends Forge2DGame { class ChromeDinoGame extends BallGame {
static const description = 'Shows how a ChromeDino is rendered.'; ChromeDinoGame()
: super(
imagesFileNames: [
Assets.images.dino.animatronic.mouth.keyName,
Assets.images.dino.animatronic.head.keyName,
],
);
static const description = '''
Shows how ChromeDino is rendered.
- Activate the "trace" parameter to overlay the body.
- Tap anywhere on the screen to spawn a ball into the game.
''';
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -10,5 +24,7 @@ class ChromeDinoGame extends Forge2DGame {
camera.followVector2(Vector2.zero()); camera.followVector2(Vector2.zero());
await add(ChromeDino()); await add(ChromeDino());
await traceAllBodies();
} }
} }

@ -4,7 +4,7 @@ import 'package:sandbox/stories/chrome_dino/chrome_dino_game.dart';
void addChromeDinoStories(Dashbook dashbook) { void addChromeDinoStories(Dashbook dashbook) {
dashbook.storiesOf('Chrome Dino').addGame( dashbook.storiesOf('Chrome Dino').addGame(
title: 'Trace', title: 'Traced',
description: ChromeDinoGame.description, description: ChromeDinoGame.description,
gameBuilder: (_) => ChromeDinoGame(), gameBuilder: (_) => ChromeDinoGame(),
); );

@ -20,8 +20,8 @@ class DinoWallGame extends BallGame {
await super.onLoad(); await super.onLoad();
await images.loadAll([ await images.loadAll([
Assets.images.dino.dinoLandTop.keyName, Assets.images.dino.topWall.keyName,
Assets.images.dino.dinoLandBottom.keyName, Assets.images.dino.bottomWall.keyName,
]); ]);
await addFromBlueprint(DinoWalls()); await addFromBlueprint(DinoWalls());

@ -1,4 +1,4 @@
export 'alien_zone/stories.dart'; export 'android_acres/stories.dart';
export 'backboard/stories.dart'; export 'backboard/stories.dart';
export 'ball/stories.dart'; export 'ball/stories.dart';
export 'baseboard/stories.dart'; export 'baseboard/stories.dart';

@ -17,7 +17,7 @@ class MockContact extends Mock implements Contact {}
class MockComponent extends Mock implements Component {} class MockComponent extends Mock implements Component {}
class MockAlienBumperCubit extends Mock implements AlienBumperCubit {} class MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {}
class MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {} class MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {}

@ -1,24 +0,0 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'AlienBumperCubit',
() {
blocTest<AlienBumperCubit, AlienBumperState>(
'onBallContacted emits inactive',
build: AlienBumperCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [AlienBumperState.inactive],
);
blocTest<AlienBumperCubit, AlienBumperState>(
'onBlinked emits active',
build: AlienBumperCubit.new,
act: (bloc) => bloc.onBlinked(),
expect: () => [AlienBumperState.active],
);
},
);
}

@ -6,48 +6,48 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/alien_bumper/behaviors/behaviors.dart'; import 'package:pinball_components/src/components/android_bumper/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.alienBumper.a.active.keyName, Assets.images.androidBumper.a.lit.keyName,
Assets.images.alienBumper.a.inactive.keyName, Assets.images.androidBumper.a.dimmed.keyName,
Assets.images.alienBumper.b.active.keyName, Assets.images.androidBumper.b.lit.keyName,
Assets.images.alienBumper.b.inactive.keyName, Assets.images.androidBumper.b.dimmed.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));
group('AlienBumper', () { group('AndroidBumper', () {
flameTester.test('"a" loads correctly', (game) async { flameTester.test('"a" loads correctly', (game) async {
final alienBumper = AlienBumper.a(); final androidBumper = AndroidBumper.a();
await game.ensureAdd(alienBumper); await game.ensureAdd(androidBumper);
expect(game.contains(alienBumper), isTrue); expect(game.contains(androidBumper), isTrue);
}); });
flameTester.test('"b" loads correctly', (game) async { flameTester.test('"b" loads correctly', (game) async {
final alienBumper = AlienBumper.b(); final androidBumper = AndroidBumper.b();
await game.ensureAdd(alienBumper); await game.ensureAdd(androidBumper);
expect(game.contains(alienBumper), isTrue); expect(game.contains(androidBumper), isTrue);
}); });
// TODO(alestiago): Consider refactoring once the following is merged: // TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = MockAlienBumperCubit(); final bloc = MockAndroidBumperCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<AlienBumperState>.empty(), const Stream<AndroidBumperState>.empty(),
initialState: AlienBumperState.active, initialState: AndroidBumperState.lit,
); );
when(bloc.close).thenAnswer((_) async {}); when(bloc.close).thenAnswer((_) async {});
final alienBumper = AlienBumper.test(bloc: bloc); final androidBumper = AndroidBumper.test(bloc: bloc);
await game.ensureAdd(alienBumper); await game.ensureAdd(androidBumper);
game.remove(alienBumper); game.remove(androidBumper);
await game.ready(); await game.ready();
verify(bloc.close).called(1); verify(bloc.close).called(1);
@ -56,19 +56,19 @@ void main() {
group('adds', () { group('adds', () {
flameTester.test('new children', (game) async { flameTester.test('new children', (game) async {
final component = Component(); final component = Component();
final alienBumper = AlienBumper.a( final androidBumper = AndroidBumper.a(
children: [component], children: [component],
); );
await game.ensureAdd(alienBumper); await game.ensureAdd(androidBumper);
expect(alienBumper.children, contains(component)); expect(androidBumper.children, contains(component));
}); });
flameTester.test('an AlienBumperBallContactBehavior', (game) async { flameTester.test('an AndroidBumperBallContactBehavior', (game) async {
final alienBumper = AlienBumper.a(); final androidBumper = AndroidBumper.a();
await game.ensureAdd(alienBumper); await game.ensureAdd(androidBumper);
expect( expect(
alienBumper.children androidBumper.children
.whereType<AlienBumperBallContactBehavior>() .whereType<AndroidBumperBallContactBehavior>()
.single, .single,
isNotNull, isNotNull,
); );

@ -5,7 +5,7 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/alien_bumper/behaviors/behaviors.dart'; import 'package:pinball_components/src/components/android_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
@ -14,33 +14,33 @@ void main() {
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
group( group(
'AlienBumperBallContactBehavior', 'AndroidBumperBallContactBehavior',
() { () {
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
AlienBumperBallContactBehavior(), AndroidBumperBallContactBehavior(),
isA<AlienBumperBallContactBehavior>(), isA<AndroidBumperBallContactBehavior>(),
); );
}); });
flameTester.test( flameTester.test(
'beginContact emits onBallContacted when contacts with a ball', 'beginContact emits onBallContacted when contacts with a ball',
(game) async { (game) async {
final behavior = AlienBumperBallContactBehavior(); final behavior = AndroidBumperBallContactBehavior();
final bloc = MockAlienBumperCubit(); final bloc = MockAndroidBumperCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<AlienBumperState>.empty(), const Stream<AndroidBumperState>.empty(),
initialState: AlienBumperState.active, initialState: AndroidBumperState.lit,
); );
final alienBumper = AlienBumper.test(bloc: bloc); final androidBumper = AndroidBumper.test(bloc: bloc);
await alienBumper.add(behavior); await androidBumper.add(behavior);
await game.ensureAdd(alienBumper); await game.ensureAdd(androidBumper);
behavior.beginContact(MockBall(), MockContact()); behavior.beginContact(MockBall(), MockContact());
verify(alienBumper.bloc.onBallContacted).called(1); verify(androidBumper.bloc.onBallContacted).called(1);
}, },
); );
}, },

@ -5,7 +5,7 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/alien_bumper/behaviors/behaviors.dart'; import 'package:pinball_components/src/components/android_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
@ -14,25 +14,25 @@ void main() {
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
group( group(
'AlienBumperBlinkingBehavior', 'AndroidBumperBlinkingBehavior',
() { () {
flameTester.testGameWidget( flameTester.testGameWidget(
'calls onBlinked after 0.05 seconds when inactive', 'calls onBlinked after 0.05 seconds when dimmed',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = AlienBumperBlinkingBehavior(); final behavior = AndroidBumperBlinkingBehavior();
final bloc = MockAlienBumperCubit(); final bloc = MockAndroidBumperCubit();
final streamController = StreamController<AlienBumperState>(); final streamController = StreamController<AndroidBumperState>();
whenListen( whenListen(
bloc, bloc,
streamController.stream, streamController.stream,
initialState: AlienBumperState.active, initialState: AndroidBumperState.lit,
); );
final alienBumper = AlienBumper.test(bloc: bloc); final androidBumper = AndroidBumper.test(bloc: bloc);
await alienBumper.add(behavior); await androidBumper.add(behavior);
await game.ensureAdd(alienBumper); await game.ensureAdd(androidBumper);
streamController.add(AlienBumperState.inactive); streamController.add(AndroidBumperState.dimmed);
await tester.pump(); await tester.pump();
game.update(0.05); game.update(0.05);

@ -0,0 +1,24 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'AndroidBumperCubit',
() {
blocTest<AndroidBumperCubit, AndroidBumperState>(
'onBallContacted emits dimmed',
build: AndroidBumperCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [AndroidBumperState.dimmed],
);
blocTest<AndroidBumperCubit, AndroidBumperState>(
'onBlinked emits lit',
build: AndroidBumperCubit.new,
act: (bloc) => bloc.onBlinked(),
expect: () => [AndroidBumperState.lit],
);
},
);
}

@ -0,0 +1,84 @@
// 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_components/src/components/bumping_behavior.dart';
import '../../helpers/helpers.dart';
import 'layer_test.dart';
class MockContactImpulse extends Mock implements ContactImpulse {}
class MockManifold extends Mock implements Manifold {}
class TestHeavyBodyComponent extends BodyComponent {
@override
Body createBody() {
final shape = CircleShape();
return world.createBody(
BodyDef(
type: BodyType.dynamic,
),
)..createFixtureFromShape(shape, 20);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('BumpingBehavior', () {
flameTester.test('can be added', (game) async {
final behavior = BumpingBehavior(strength: 0);
final component = TestBodyComponent();
await component.add(behavior);
await game.ensureAdd(component);
});
flameTester.testGameWidget(
'the bump is greater when the strengh is greater',
setUp: (game, tester) async {
final component1 = TestBodyComponent();
final behavior1 = BumpingBehavior(strength: 1);
await component1.add(behavior1);
final component2 = TestBodyComponent();
final behavior2 = BumpingBehavior(strength: 2);
await component2.add(behavior2);
final dummy1 = TestHeavyBodyComponent();
final dummy2 = TestHeavyBodyComponent();
await game.ensureAddAll([
component1,
component2,
dummy1,
dummy2,
]);
expect(dummy1.body.inverseMass, greaterThan(0));
expect(dummy2.body.inverseMass, greaterThan(0));
final contact = MockContact();
final manifold = MockManifold();
final contactImpulse = MockContactImpulse();
when(() => manifold.localPoint).thenReturn(Vector2.all(1));
when(() => contact.manifold).thenReturn(manifold);
behavior1.postSolve(dummy1, contact, contactImpulse);
behavior2.postSolve(dummy2, contact, contactImpulse);
expect(
dummy2.body.linearVelocity.x,
greaterThan(dummy1.body.linearVelocity.x),
);
expect(
dummy2.body.linearVelocity.y,
greaterThan(dummy1.body.linearVelocity.y),
);
},
);
});
}

@ -1,13 +1,19 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.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:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new); final assets = [
Assets.images.dino.animatronic.mouth.keyName,
Assets.images.dino.animatronic.head.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('ChromeDino', () { group('ChromeDino', () {
flameTester.test( flameTester.test(
@ -20,19 +26,84 @@ void main() {
}, },
); );
flameTester.test( flameTester.testGameWidget(
'swivels', 'renders correctly',
(game) async { setUp: (game, tester) async {
// TODO(alestiago): Write golden tests to check the await game.images.loadAll(assets);
// swivel animation. await game.ensureAdd(ChromeDino());
final chromeDino = ChromeDino(); game.camera.followVector2(Vector2.zero());
await game.ensureAdd(chromeDino); await tester.pump();
},
verify: (game, tester) async {
final sweepAnimationDuration = game
.descendants()
.whereType<SpriteAnimationComponent>()
.first
.animation!
.totalDuration() /
2;
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/chrome_dino/up.png'),
);
final previousPosition = chromeDino.body.position.clone(); game.update(sweepAnimationDuration * 0.25);
game.update(64); await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/chrome_dino/middle.png'),
);
expect(chromeDino.body.position, isNot(equals(previousPosition))); game.update(sweepAnimationDuration * 0.25);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/chrome_dino/down.png'),
);
}, },
); );
group('swivels', () {
flameTester.test(
'up',
(game) async {
final chromeDino = ChromeDino();
await game.ensureAdd(chromeDino);
game.camera.followVector2(Vector2.zero());
final sweepAnimationDuration = game
.descendants()
.whereType<SpriteAnimationComponent>()
.first
.animation!
.totalDuration() /
2;
game.update(sweepAnimationDuration * 1.5);
expect(chromeDino.body.angularVelocity, isPositive);
},
);
flameTester.test(
'down',
(game) async {
final chromeDino = ChromeDino();
await game.ensureAdd(chromeDino);
game.camera.followVector2(Vector2.zero());
final sweepAnimationDuration = game
.descendants()
.whereType<SpriteAnimationComponent>()
.first
.animation!
.totalDuration() /
2;
game.update(sweepAnimationDuration * 0.5);
expect(chromeDino.body.angularVelocity, isNegative);
},
);
});
}); });
} }

@ -45,6 +45,7 @@ void main() {
); );
}, },
); );
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {

@ -12,8 +12,8 @@ void main() {
group('DinoWalls', () { group('DinoWalls', () {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.dino.dinoLandTop.keyName, Assets.images.dino.topWall.keyName,
Assets.images.dino.dinoLandBottom.keyName, Assets.images.dino.bottomWall.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 92 KiB

@ -11,13 +11,19 @@ import '../../helpers/helpers.dart';
void main() { void main() {
group('SpaceshipRail', () { group('SpaceshipRail', () {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final assets = [
Assets.images.spaceship.rail.main.keyName,
Assets.images.spaceship.rail.exit.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.testGameWidget( flameTester.testGameWidget(
'renders correctly', 'renders correctly',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.addFromBlueprint(SpaceshipRail()); await game.addFromBlueprint(SpaceshipRail());
await game.ready(); await game.ready();
await tester.pump();
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
game.camera.zoom = 8; game.camera.zoom = 8;

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared name: _fe_analyzer_shared
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "39.0.0" version: "31.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.0" version: "2.8.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -71,6 +71,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -316,7 +323,7 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.4" version: "0.6.3"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@ -351,7 +358,7 @@ packages:
name: material_color_utilities name: material_color_utilities
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.4" version: "0.1.3"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -414,7 +421,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.0"
path_provider: path_provider:
dependency: transitive dependency: transitive
description: description:
@ -587,7 +594,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.8.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -629,21 +636,21 @@ packages:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.21.1" version: "1.19.5"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.9" version: "0.4.8"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.13" version: "0.4.9"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -664,7 +671,7 @@ packages:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.1"
very_good_analysis: very_good_analysis:
dependency: "direct dev" dependency: "direct dev"
description: description:

@ -4,27 +4,69 @@ import 'package:pinball/game/game.dart';
void main() { void main() {
group('GameBloc', () { group('GameBloc', () {
test('initial state has 3 balls 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.score, equals(0));
expect(gameBloc.state.balls, equals(3)); expect(gameBloc.state.rounds, equals(3));
}); });
group('LostBall', () { group('RoundLost', () {
blocTest<GameBloc, GameState>( blocTest<GameBloc, GameState>(
'decreases number of balls', 'decreases number of rounds '
'when there are already available rounds',
build: GameBloc.new, build: GameBloc.new,
act: (bloc) { act: (bloc) {
bloc.add(const BallLost()); bloc.add(const RoundLost());
}, },
expect: () => [ expect: () => [
const GameState( const GameState(
score: 0, score: 0,
balls: 2, multiplier: 1,
rounds: 2,
bonusHistory: [], bonusHistory: [],
), ),
], ],
); );
blocTest<GameBloc, GameState>(
'apply multiplier to score '
'when round is lost',
build: GameBloc.new,
seed: () => const GameState(
score: 5,
multiplier: 3,
rounds: 2,
bonusHistory: [],
),
act: (bloc) {
bloc.add(const RoundLost());
},
expect: () => [
isA<GameState>()
..having((state) => state.score, 'score', 15)
..having((state) => state.rounds, 'rounds', 1),
],
);
blocTest<GameBloc, GameState>(
'resets multiplier '
'when round is lost',
build: GameBloc.new,
seed: () => const GameState(
score: 5,
multiplier: 3,
rounds: 2,
bonusHistory: [],
),
act: (bloc) {
bloc.add(const RoundLost());
},
expect: () => [
isA<GameState>()
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.rounds, 'rounds', 1),
],
);
}); });
group('Scored', () { group('Scored', () {
@ -36,16 +78,12 @@ void main() {
..add(const Scored(points: 2)) ..add(const Scored(points: 2))
..add(const Scored(points: 3)), ..add(const Scored(points: 3)),
expect: () => [ expect: () => [
const GameState( isA<GameState>()
score: 2, ..having((state) => state.score, 'score', 2)
balls: 3, ..having((state) => state.isGameOver, 'isGameOver', false),
bonusHistory: [], isA<GameState>()
), ..having((state) => state.score, 'score', 5)
const GameState( ..having((state) => state.isGameOver, 'isGameOver', false),
score: 5,
balls: 3,
bonusHistory: [],
),
], ],
); );
@ -54,27 +92,85 @@ void main() {
'when game is over', 'when game is over',
build: GameBloc.new, build: GameBloc.new,
act: (bloc) { act: (bloc) {
for (var i = 0; i < bloc.state.balls; i++) { for (var i = 0; i < bloc.state.rounds; i++) {
bloc.add(const BallLost()); bloc.add(const RoundLost());
} }
bloc.add(const Scored(points: 2)); bloc.add(const Scored(points: 2));
}, },
expect: () => [ expect: () => [
const GameState( isA<GameState>()
score: 0, ..having((state) => state.score, 'score', 0)
balls: 2, ..having((state) => state.rounds, 'rounds', 2)
bonusHistory: [], ..having((state) => state.isGameOver, 'isGameOver', false),
), isA<GameState>()
const GameState( ..having((state) => state.score, 'score', 0)
score: 0, ..having((state) => state.rounds, 'rounds', 1)
balls: 1, ..having((state) => state.isGameOver, 'isGameOver', false),
bonusHistory: [], isA<GameState>()
), ..having((state) => state.score, 'score', 0)
const GameState( ..having((state) => state.rounds, 'rounds', 0)
score: 0, ..having((state) => state.isGameOver, 'isGameOver', true),
balls: 0, ],
bonusHistory: [], );
), });
group('MultiplierIncreased', () {
blocTest<GameBloc, GameState>(
'increases multiplier '
'when multiplier is below 6 and game is not over',
build: GameBloc.new,
act: (bloc) => bloc
..add(const MultiplierIncreased())
..add(const MultiplierIncreased()),
expect: () => [
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 2)
..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 3)
..having((state) => state.isGameOver, 'isGameOver', false),
],
);
blocTest<GameBloc, GameState>(
"doesn't increase multiplier "
'when multiplier is 6 and game is not over',
build: GameBloc.new,
seed: () => const GameState(
score: 0,
multiplier: 6,
rounds: 3,
bonusHistory: [],
),
act: (bloc) => bloc..add(const MultiplierIncreased()),
expect: () => const <GameState>[],
);
blocTest<GameBloc, GameState>(
"doesn't increase multiplier "
'when game is over',
build: GameBloc.new,
act: (bloc) {
for (var i = 0; i < bloc.state.rounds; i++) {
bloc.add(const RoundLost());
}
bloc.add(const MultiplierIncreased());
},
expect: () => [
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', true),
], ],
); );
}); });
@ -88,17 +184,19 @@ void main() {
act: (bloc) => bloc act: (bloc) => bloc
..add(const BonusActivated(GameBonus.googleWord)) ..add(const BonusActivated(GameBonus.googleWord))
..add(const BonusActivated(GameBonus.dashNest)), ..add(const BonusActivated(GameBonus.dashNest)),
expect: () => const [ expect: () => [
GameState( isA<GameState>()
score: 0, ..having(
balls: 3, (state) => state.bonusHistory,
bonusHistory: [GameBonus.googleWord], 'bonusHistory',
), [GameBonus.googleWord],
GameState( ),
score: 0, isA<GameState>()
balls: 3, ..having(
bonusHistory: [GameBonus.googleWord, GameBonus.dashNest], (state) => state.bonusHistory,
), 'bonusHistory',
[GameBonus.googleWord, GameBonus.dashNest],
),
], ],
); );
}, },
@ -109,12 +207,13 @@ void main() {
'adds game bonus', 'adds game bonus',
build: GameBloc.new, build: GameBloc.new,
act: (bloc) => bloc..add(const SparkyTurboChargeActivated()), act: (bloc) => bloc..add(const SparkyTurboChargeActivated()),
expect: () => const [ expect: () => [
GameState( isA<GameState>()
score: 0, ..having(
balls: 3, (state) => state.bonusHistory,
bonusHistory: [GameBonus.sparkyTurboCharge], 'bonusHistory',
), [GameBonus.sparkyTurboCharge],
),
], ],
); );
}); });

@ -5,15 +5,15 @@ import 'package:pinball/game/game.dart';
void main() { void main() {
group('GameEvent', () { group('GameEvent', () {
group('BallLost', () { group('RoundLost', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(const BallLost(), isNotNull); expect(const RoundLost(), isNotNull);
}); });
test('supports value equality', () { test('supports value equality', () {
expect( expect(
BallLost(), RoundLost(),
equals(const BallLost()), equals(const RoundLost()),
); );
}); });
}); });
@ -41,6 +41,19 @@ void main() {
}); });
}); });
group('MultiplierIncreased', () {
test('can be instantiated', () {
expect(const MultiplierIncreased(), isNotNull);
});
test('supports value equality', () {
expect(
MultiplierIncreased(),
equals(const MultiplierIncreased()),
);
});
});
group('BonusActivated', () { group('BonusActivated', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(const BonusActivated(GameBonus.dashNest), isNotNull); expect(const BonusActivated(GameBonus.dashNest), isNotNull);

@ -9,13 +9,15 @@ void main() {
expect( expect(
GameState( GameState(
score: 0, score: 0,
balls: 0, multiplier: 1,
rounds: 3,
bonusHistory: const [], bonusHistory: const [],
), ),
equals( equals(
const GameState( const GameState(
score: 0, score: 0,
balls: 0, multiplier: 1,
rounds: 3,
bonusHistory: [], bonusHistory: [],
), ),
), ),
@ -27,7 +29,8 @@ void main() {
expect( expect(
const GameState( const GameState(
score: 0, score: 0,
balls: 0, multiplier: 1,
rounds: 3,
bonusHistory: [], bonusHistory: [],
), ),
isNotNull, isNotNull,
@ -37,12 +40,13 @@ void main() {
test( test(
'throws AssertionError ' 'throws AssertionError '
'when balls are negative', 'when score is negative',
() { () {
expect( expect(
() => GameState( () => GameState(
balls: -1, score: -1,
score: 0, multiplier: 1,
rounds: 3,
bonusHistory: const [], bonusHistory: const [],
), ),
throwsAssertionError, throwsAssertionError,
@ -52,12 +56,29 @@ void main() {
test( test(
'throws AssertionError ' 'throws AssertionError '
'when score is negative', 'when multiplier is less than 1',
() { () {
expect( expect(
() => GameState( () => GameState(
balls: 0, score: 1,
score: -1, multiplier: 0,
rounds: 3,
bonusHistory: const [],
),
throwsAssertionError,
);
},
);
test(
'throws AssertionError '
'when rounds is negative',
() {
expect(
() => GameState(
score: 1,
multiplier: 1,
rounds: -1,
bonusHistory: const [], bonusHistory: const [],
), ),
throwsAssertionError, throwsAssertionError,
@ -68,10 +89,11 @@ void main() {
group('isGameOver', () { group('isGameOver', () {
test( test(
'is true ' 'is true '
'when no balls are left', () { 'when no rounds are left', () {
const gameState = GameState( const gameState = GameState(
balls: 0,
score: 0, score: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isGameOver, isTrue); expect(gameState.isGameOver, isTrue);
@ -79,10 +101,11 @@ void main() {
test( test(
'is false ' 'is false '
'when one 1 ball left', () { 'when one 1 round left', () {
const gameState = GameState( const gameState = GameState(
balls: 1,
score: 0, score: 0,
multiplier: 1,
rounds: 1,
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isGameOver, isFalse); expect(gameState.isGameOver, isFalse);
@ -95,8 +118,9 @@ void main() {
'when scored is decreased', 'when scored is decreased',
() { () {
const gameState = GameState( const gameState = GameState(
balls: 0,
score: 2, score: 2,
multiplier: 1,
rounds: 3,
bonusHistory: [], bonusHistory: [],
); );
expect( expect(
@ -111,8 +135,9 @@ void main() {
'when no argument specified', 'when no argument specified',
() { () {
const gameState = GameState( const gameState = GameState(
balls: 0,
score: 2, score: 2,
multiplier: 1,
rounds: 3,
bonusHistory: [], bonusHistory: [],
); );
expect( expect(
@ -128,12 +153,14 @@ void main() {
() { () {
const gameState = GameState( const gameState = GameState(
score: 2, score: 2,
balls: 0, multiplier: 1,
rounds: 3,
bonusHistory: [], bonusHistory: [],
); );
final otherGameState = GameState( final otherGameState = GameState(
score: gameState.score + 1, score: gameState.score + 1,
balls: gameState.balls + 1, multiplier: gameState.multiplier + 1,
rounds: gameState.rounds + 1,
bonusHistory: const [GameBonus.googleWord], bonusHistory: const [GameBonus.googleWord],
); );
expect(gameState, isNot(equals(otherGameState))); expect(gameState, isNot(equals(otherGameState)));
@ -141,7 +168,8 @@ void main() {
expect( expect(
gameState.copyWith( gameState.copyWith(
score: otherGameState.score, score: otherGameState.score,
balls: otherGameState.balls, multiplier: otherGameState.multiplier,
rounds: otherGameState.rounds,
bonusHistory: otherGameState.bonusHistory, bonusHistory: otherGameState.bonusHistory,
), ),
equals(otherGameState), equals(otherGameState),

@ -1,48 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.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';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.alienBumper.a.active.keyName,
Assets.images.alienBumper.a.inactive.keyName,
Assets.images.alienBumper.b.active.keyName,
Assets.images.alienBumper.b.inactive.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('AlienZone', () {
flameTester.test(
'loads correctly',
(game) async {
await game.addFromBlueprint(AlienZone());
await game.ready();
},
);
group('loads', () {
flameTester.test(
'two AlienBumper',
(game) async {
final alienZone = AlienZone();
await game.addFromBlueprint(alienZone);
await game.ready();
expect(
game.descendants().whereType<AlienBumper>().length,
equals(2),
);
},
);
});
});
}

@ -0,0 +1,90 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.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';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.spaceship.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.railingBackground.keyName,
Assets.images.spaceship.ramp.main.keyName,
Assets.images.spaceship.ramp.arrow.inactive.keyName,
Assets.images.spaceship.ramp.arrow.active1.keyName,
Assets.images.spaceship.ramp.arrow.active2.keyName,
Assets.images.spaceship.ramp.arrow.active3.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName,
Assets.images.spaceship.rail.main.keyName,
Assets.images.spaceship.rail.exit.keyName,
Assets.images.androidBumper.a.lit.keyName,
Assets.images.androidBumper.a.dimmed.keyName,
Assets.images.androidBumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('AndroidAcres', () {
flameTester.test(
'loads correctly',
(game) async {
await game.addFromBlueprint(AndroidAcres());
await game.ready();
},
);
group('loads', () {
flameTester.test(
'a Spaceship',
(game) async {
expect(
AndroidAcres().blueprints.whereType<Spaceship>().single,
isNotNull,
);
},
);
flameTester.test(
'a SpaceshipRamp',
(game) async {
expect(
AndroidAcres().blueprints.whereType<SpaceshipRamp>().single,
isNotNull,
);
},
);
flameTester.test(
'a SpaceshipRail',
(game) async {
expect(
AndroidAcres().blueprints.whereType<SpaceshipRail>().single,
isNotNull,
);
},
);
flameTester.test(
'two AndroidBumper',
(game) async {
final androidZone = AndroidAcres();
await game.addFromBlueprint(androidZone);
await game.ready();
expect(
game.descendants().whereType<AndroidBumper>().length,
equals(2),
);
},
);
});
});
}

@ -105,18 +105,6 @@ void main() {
expect(flutterForest.length, equals(1)); expect(flutterForest.length, equals(1));
}, },
); );
flameTester.test(
'one ChromeDino',
(game) async {
final board = Board();
await game.ready();
await game.ensureAdd(board);
final chromeDino = board.descendants().whereType<ChromeDino>();
expect(chromeDino.length, equals(1));
},
);
}); });
}); });
} }

@ -53,16 +53,39 @@ void main() {
}); });
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'lost adds BallLost to GameBloc', "lost doesn't adds RoundLost to GameBloc "
'when there are balls left',
setUp: (game, tester) async {
final controller = BallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final otherBall = Ball(baseColor: const Color(0xFF00FFFF));
final otherController = BallController(otherBall);
await otherBall.add(otherController);
await game.ensureAdd(otherBall);
controller.lost();
await game.ready();
},
verify: (game, tester) async {
verifyNever(() => gameBloc.add(const RoundLost()));
},
);
flameBlocTester.testGameWidget(
'lost adds RoundLost to GameBloc '
'when there are no balls left',
setUp: (game, tester) async { setUp: (game, tester) async {
final controller = BallController(ball); final controller = BallController(ball);
await ball.add(controller); await ball.add(controller);
await game.ensureAdd(ball); await game.ensureAdd(ball);
controller.lost(); controller.lost();
await game.ready();
}, },
verify: (game, tester) async { verify: (game, tester) async {
verify(() => gameBloc.add(const BallLost())).called(1); verify(() => gameBloc.add(const RoundLost())).called(1);
}, },
); );

@ -25,7 +25,8 @@ void main() {
final bloc = MockGameBloc(); final bloc = MockGameBloc();
const state = GameState( const state = GameState(
score: 0, score: 0,
balls: 0, multiplier: 1,
rounds: 0,
bonusHistory: [], bonusHistory: [],
); );
whenListen(bloc, Stream.value(state), initialState: state); whenListen(bloc, Stream.value(state), initialState: state);

@ -20,7 +20,8 @@ void main() {
final bloc = MockGameBloc(); final bloc = MockGameBloc();
const state = GameState( const state = GameState(
score: 0, score: 0,
balls: 0, multiplier: 1,
rounds: 0,
bonusHistory: [], bonusHistory: [],
); );
whenListen(bloc, Stream.value(state), initialState: state); whenListen(bloc, Stream.value(state), initialState: state);

@ -15,7 +15,8 @@ void main() {
test('is true when the game over state has changed', () { test('is true when the game over state has changed', () {
final state = GameState( final state = GameState(
score: 10, score: 10,
balls: 0, multiplier: 1,
rounds: 0,
bonusHistory: const [], bonusHistory: const [],
); );
@ -66,7 +67,8 @@ void main() {
gameFlowController.onNewState( gameFlowController.onNewState(
GameState( GameState(
score: 10, score: 10,
balls: 0, multiplier: 1,
rounds: 0,
bonusHistory: const [], bonusHistory: const [],
), ),
); );

@ -43,7 +43,8 @@ void main() {
bloc = MockGameBloc(); bloc = MockGameBloc();
const state = GameState( const state = GameState(
score: 0, score: 0,
balls: 0, multiplier: 1,
rounds: 3,
bonusHistory: [], bonusHistory: [],
); );
whenListen(bloc, Stream.value(state), initialState: state); whenListen(bloc, Stream.value(state), initialState: state);

@ -146,9 +146,9 @@ void main() {
}, },
); );
flameTester.test( flameBlocTester.testGameWidget(
'when ball is debug', 'when ball is debug',
(game) async { setUp: (game, tester) async {
final ball = ControlledBall.debug(); final ball = ControlledBall.debug();
final wall = BottomWall(); final wall = BottomWall();
await game.ensureAddAll([ball, wall]); await game.ensureAddAll([ball, wall]);

@ -13,28 +13,54 @@ import '../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.dash.bumper.main.active.keyName, Assets.images.androidBumper.a.lit.keyName,
Assets.images.dash.bumper.main.inactive.keyName, Assets.images.androidBumper.a.dimmed.keyName,
Assets.images.androidBumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName,
Assets.images.backboard.backboardScores.keyName,
Assets.images.backboard.backboardGameOver.keyName,
Assets.images.backboard.display.keyName,
Assets.images.ball.ball.keyName,
Assets.images.ball.flameEffect.keyName,
Assets.images.baseboard.left.keyName,
Assets.images.baseboard.right.keyName,
Assets.images.boundary.bottom.keyName,
Assets.images.boundary.outer.keyName,
Assets.images.boundary.outerBottom.keyName,
Assets.images.dino.animatronic.mouth.keyName,
Assets.images.dino.animatronic.head.keyName,
Assets.images.dino.topWall.keyName,
Assets.images.dino.bottomWall.keyName,
Assets.images.dash.animatronic.keyName,
Assets.images.dash.bumper.a.active.keyName, Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName, Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName, Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName, Assets.images.dash.bumper.b.inactive.keyName,
Assets.images.dash.animatronic.keyName, Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName,
Assets.images.googleWord.letter1.keyName,
Assets.images.googleWord.letter2.keyName,
Assets.images.googleWord.letter3.keyName,
Assets.images.googleWord.letter4.keyName,
Assets.images.googleWord.letter5.keyName,
Assets.images.googleWord.letter6.keyName,
Assets.images.kicker.left.keyName,
Assets.images.kicker.right.keyName,
Assets.images.launchRamp.ramp.keyName,
Assets.images.launchRamp.foregroundRailing.keyName,
Assets.images.launchRamp.backgroundRailing.keyName,
Assets.images.plunger.plunger.keyName,
Assets.images.plunger.rocket.keyName,
Assets.images.signpost.inactive.keyName, Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName, Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName, Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName, Assets.images.signpost.active3.keyName,
Assets.images.alienBumper.a.active.keyName, Assets.images.slingshot.upper.keyName,
Assets.images.alienBumper.a.inactive.keyName, Assets.images.slingshot.lower.keyName,
Assets.images.alienBumper.b.active.keyName, Assets.images.spaceship.saucer.keyName,
Assets.images.alienBumper.b.inactive.keyName, Assets.images.spaceship.bridge.keyName,
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
Assets.images.sparky.animatronic.keyName,
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.spaceship.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.spaceship.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.spaceship.ramp.railingBackground.keyName,
@ -45,18 +71,26 @@ void main() {
Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.spaceship.ramp.arrow.active3.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.spaceship.ramp.arrow.active4.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName, Assets.images.spaceship.ramp.arrow.active5.keyName,
Assets.images.baseboard.left.keyName, Assets.images.spaceship.rail.main.keyName,
Assets.images.baseboard.right.keyName, Assets.images.spaceship.rail.exit.keyName,
Assets.images.flipper.left.keyName, Assets.images.sparky.bumper.a.active.keyName,
Assets.images.flipper.right.keyName, Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.boundary.outer.keyName, Assets.images.sparky.bumper.b.active.keyName,
Assets.images.boundary.outerBottom.keyName, Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.boundary.bottom.keyName, Assets.images.sparky.bumper.c.active.keyName,
Assets.images.slingshot.upper.keyName, Assets.images.sparky.bumper.c.inactive.keyName,
Assets.images.slingshot.lower.keyName, Assets.images.sparky.animatronic.keyName,
Assets.images.dino.dinoLandTop.keyName, Assets.images.sparky.computer.top.keyName,
Assets.images.dino.dinoLandBottom.keyName, Assets.images.sparky.computer.base.keyName,
Assets.images.sparky.animatronic.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
]; ];
final flameTester = FlameTester( final flameTester = FlameTester(
() => PinballTestGame(assets: assets), () => PinballTestGame(assets: assets),
); );
@ -72,7 +106,6 @@ void main() {
'has only one BottomWall', 'has only one BottomWall',
(game) async { (game) async {
await game.ready(); await game.ready();
expect( expect(
game.children.whereType<BottomWall>().length, game.children.whereType<BottomWall>().length,
equals(1), equals(1),
@ -91,34 +124,34 @@ void main() {
}, },
); );
flameTester.test('has one Board', (game) async { flameTester.test(
await game.ready(); 'has one Board',
expect( (game) async {
game.children.whereType<Board>().length, await game.ready();
equals(1), expect(
); game.children.whereType<Board>().length,
}); equals(1),
);
},
);
flameTester.test(
'one GoogleWord',
(game) async {
await game.ready();
expect(game.children.whereType<GoogleWord>().length, equals(1));
},
);
group('controller', () { group('controller', () {
// TODO(alestiago): Write test to be controller agnostic.
group('listenWhen', () { group('listenWhen', () {
late GameBloc gameBloc; flameTester.testGameWidget(
'listens when all balls are lost and there are more than 0 rounds',
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
// assets: assets,
);
flameBlocTester.testGameWidget(
'listens when all balls are lost and there are more than 0 balls',
setUp: (game, tester) async { setUp: (game, tester) async {
// TODO(ruimiguel): check why testGameWidget doesn't add any ball
// to the game. Test needs to have no balls, so fortunately works.
final newState = MockGameState(); final newState = MockGameState();
when(() => newState.balls).thenReturn(2); when(() => newState.isGameOver).thenReturn(false);
game.descendants().whereType<ControlledBall>().forEach( game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(), (ball) => ball.controller.lost(),
); );
@ -135,10 +168,10 @@ void main() {
"doesn't listen when some balls are left", "doesn't listen when some balls are left",
(game) async { (game) async {
final newState = MockGameState(); final newState = MockGameState();
when(() => newState.balls).thenReturn(1); when(() => newState.isGameOver).thenReturn(false);
expect( expect(
game.descendants().whereType<Ball>().length, game.descendants().whereType<ControlledBall>().length,
greaterThan(0), greaterThan(0),
); );
expect( expect(
@ -148,19 +181,20 @@ void main() {
}, },
); );
flameBlocTester.test( flameTester.testGameWidget(
"doesn't listen when no balls left", "doesn't listen when game is over",
(game) async { setUp: (game, tester) async {
// TODO(ruimiguel): check why testGameWidget doesn't add any ball
// to the game. Test needs to have no balls, so fortunately works.
final newState = MockGameState(); final newState = MockGameState();
when(() => newState.balls).thenReturn(0); when(() => newState.isGameOver).thenReturn(true);
game.descendants().whereType<ControlledBall>().forEach( game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(), (ball) => ball.controller.lost(),
); );
await game.ready(); await game.ready();
expect( expect(
game.descendants().whereType<Ball>().isEmpty, game.descendants().whereType<ControlledBall>().isEmpty,
isTrue, isTrue,
); );
expect( expect(
@ -177,14 +211,13 @@ void main() {
flameTester.test( flameTester.test(
'spawns a ball', 'spawns a ball',
(game) async { (game) async {
await game.ready();
final previousBalls = final previousBalls =
game.descendants().whereType<Ball>().toList(); game.descendants().whereType<ControlledBall>().toList();
game.controller.onNewState(MockGameState()); game.controller.onNewState(MockGameState());
await game.ready(); await game.ready();
final currentBalls = final currentBalls =
game.descendants().whereType<Ball>().toList(); game.descendants().whereType<ControlledBall>().toList();
expect( expect(
currentBalls.length, currentBalls.length,
@ -199,57 +232,26 @@ void main() {
}); });
group('DebugPinballGame', () { group('DebugPinballGame', () {
debugModeFlameTester.test('adds a ball on tap up', (game) async { debugModeFlameTester.test(
await game.ready(); 'adds a ball on tap up',
(game) async {
final eventPosition = MockEventPosition(); final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.all(10)); when(() => eventPosition.game).thenReturn(Vector2.all(10));
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
final previousBalls = game.descendants().whereType<Ball>().toList();
game.onTapUp(tapUpEvent);
await game.ready();
expect( final tapUpEvent = MockTapUpInfo();
game.children.whereType<Ball>().length, when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
equals(previousBalls.length + 1),
);
});
group('controller', () {
late GameBloc gameBloc;
setUp(() { final previousBalls =
gameBloc = GameBloc(); game.descendants().whereType<ControlledBall>().toList();
});
final debugModeFlameBlocTester =
FlameBlocTester<DebugPinballGame, GameBloc>(
gameBuilder: DebugPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
debugModeFlameBlocTester.testGameWidget( game.onTapUp(tapUpEvent);
'ignores debug balls', await game.ready();
setUp: (game, tester) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(1);
await game.ready();
game.children.removeWhere((component) => component is Ball);
await game.ready();
await game.ensureAdd(ControlledBall.debug());
expect( expect(
game.controller.listenWhen(MockGameState(), newState), game.children.whereType<ControlledBall>().length,
isTrue, equals(previousBalls.length + 1),
); );
}, },
); );
});
}); });
} }

@ -28,7 +28,8 @@ void main() {
const initialState = GameState( const initialState = GameState(
score: 1000, score: 1000,
balls: 2, multiplier: 1,
rounds: 1,
bonusHistory: [], bonusHistory: [],
); );

@ -11,7 +11,8 @@ void main() {
late GameBloc gameBloc; late GameBloc gameBloc;
const initialState = GameState( const initialState = GameState(
score: 0, score: 0,
balls: 3, multiplier: 1,
rounds: 3,
bonusHistory: [], bonusHistory: [],
); );
@ -37,7 +38,7 @@ void main() {
testWidgets('two active round indicator', (tester) async { testWidgets('two active round indicator', (tester) async {
final state = initialState.copyWith( final state = initialState.copyWith(
balls: 2, rounds: 2,
); );
whenListen( whenListen(
gameBloc, gameBloc,
@ -68,7 +69,7 @@ void main() {
testWidgets('one active round indicator', (tester) async { testWidgets('one active round indicator', (tester) async {
final state = initialState.copyWith( final state = initialState.copyWith(
balls: 1, rounds: 1,
); );
whenListen( whenListen(
gameBloc, gameBloc,

@ -15,7 +15,8 @@ void main() {
const score = 123456789; const score = 123456789;
const initialState = GameState( const initialState = GameState(
score: score, score: score,
balls: 1, multiplier: 1,
rounds: 1,
bonusHistory: [], bonusHistory: [],
); );
@ -46,7 +47,7 @@ void main() {
stateController.add( stateController.add(
initialState.copyWith( initialState.copyWith(
balls: 0, rounds: 0,
), ),
); );

@ -82,6 +82,6 @@ class MockActiveOverlaysNotifier extends Mock
class MockGameFlowController extends Mock implements GameFlowController {} class MockGameFlowController extends Mock implements GameFlowController {}
class MockAlienBumper extends Mock implements AlienBumper {} class MockAndroidBumper extends Mock implements AndroidBumper {}
class MockSparkyBumper extends Mock implements SparkyBumper {} class MockSparkyBumper extends Mock implements SparkyBumper {}

Loading…
Cancel
Save