fix: fixed backbox conflicts when merge with main

pull/359/head
RuiAlonso 3 years ago
commit fa42261ebd

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

@ -1,27 +1,39 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
part 'assets_manager_state.dart';
/// {@template assets_manager_cubit}
/// Cubit responsable for pre loading any game assets
/// {@endtemplate}
class AssetsManagerCubit extends Cubit<AssetsManagerState> {
/// {@macro assets_manager_cubit}
AssetsManagerCubit(List<Future> loadables)
: super(
AssetsManagerState.initial(
loadables: loadables,
),
);
AssetsManagerCubit(this._game, this._player)
: super(const AssetsManagerState.initial());
final PinballGame _game;
final PinballPlayer _player;
/// Loads the assets
Future<void> load() async {
/// Assigning loadables is a very expensive operation. With this purposeful
/// delay here, which is a bit random in duration but enough to let the UI
/// do its job without adding too much delay for the user, we are letting
/// the UI paint first, and then we start loading the assets.
await Future<void>.delayed(const Duration(milliseconds: 300));
emit(
state.copyWith(
loadables: [
_game.preFetchLeaderboard(),
..._game.preLoadAssets(),
..._player.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
],
),
);
final all = state.loadables.map((loadable) async {
await loadable;
emit(state.copyWith(loaded: [...state.loaded, loadable]));
}).toList();
await Future.wait(all);
}
}

@ -11,9 +11,8 @@ class AssetsManagerState extends Equatable {
});
/// {@macro assets_manager_state}
const AssetsManagerState.initial({
required List<Future> loadables,
}) : this(loadables: loadables, loaded: const []);
const AssetsManagerState.initial()
: this(loadables: const [], loaded: const []);
/// List of futures to load
final List<Future> loadables;
@ -22,7 +21,11 @@ class AssetsManagerState extends Equatable {
final List<Future> loaded;
/// Returns a value between 0 and 1 to indicate the loading progress
double get progress => loaded.length / loadables.length;
double get progress =>
loadables.isEmpty ? 0 : loaded.length / loadables.length;
/// Only returns false if all the assets have been loaded
bool get isLoading => progress != 1;
/// Returns a copy of this instance with the given parameters
/// updated

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
@ -20,10 +21,9 @@ class AssetsLoadingPage extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.ioPinball,
style: headline1!.copyWith(fontSize: 80),
textAlign: TextAlign.center,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Assets.images.loadingGame.ioPinball.image(),
),
const SizedBox(height: 40),
AnimatedEllipsisText(

@ -1,5 +1,6 @@
export 'ball_spawning_behavior.dart';
export 'ball_theming_behavior.dart';
export 'bonus_ball_spawning_behavior.dart';
export 'bonus_noise_behavior.dart';
export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart';

@ -0,0 +1,30 @@
import 'package:flame/components.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template bonus_ball_spawning_behavior}
/// After a duration, spawns a bonus ball from the [DinoWalls] and boosts it
/// into the middle of the board.
/// {@endtemplate}
class BonusBallSpawningBehavior extends TimerComponent with HasGameRef {
/// {@macro bonus_ball_spawning_behavior}
BonusBallSpawningBehavior()
: super(
period: 5,
removeOnFinish: true,
);
@override
void onTick() {
final characterTheme = readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme;
gameRef.descendants().whereType<ZCanvasComponent>().single.add(
Ball(assetPath: characterTheme.ball.keyName)
..add(BallImpulsingBehavior(impulse: Vector2(-40, 0)))
..initialPosition = Vector2(29.2, -24.5)
..zIndex = ZIndexes.ballOnBoard,
);
}
}

@ -25,10 +25,13 @@ class BonusNoiseBehavior extends Component {
audioPlayer.play(PinballAudio.sparky);
break;
case GameBonus.dinoChomp:
audioPlayer.play(PinballAudio.dino);
break;
case GameBonus.androidSpaceship:
audioPlayer.play(PinballAudio.android);
break;
case GameBonus.dashNest:
audioPlayer.play(PinballAudio.dash);
break;
}
},

@ -5,7 +5,7 @@ enum GameBonus {
/// Bonus achieved when the ball activates all Google letters.
googleWord,
/// Bonus achieved when the user activates all dash nest bumpers.
/// Bonus achieved when the user activates all dash bumpers.
dashNest,
/// Bonus achieved when a ball enters Sparky's computer.

@ -1,6 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
@ -15,42 +16,43 @@ class AndroidAcres extends Component {
AndroidAcres()
: super(
children: [
SpaceshipRamp(
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>(
create: AndroidSpaceshipCubit.new,
children: [
RampShotBehavior(
points: Points.fiveThousand,
),
RampBonusBehavior(
points: Points.oneMillion,
SpaceshipRamp(
children: [
RampShotBehavior(points: Points.fiveThousand),
RampBonusBehavior(points: Points.oneMillion),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(),
],
);

@ -5,18 +5,21 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus.
class AndroidSpaceshipBonusBehavior extends Component
with ParentIsA<AndroidAcres>, FlameBlocReader<GameBloc, GameState> {
class AndroidSpaceshipBonusBehavior extends Component {
@override
void onMount() {
super.onMount();
final androidSpaceship = parent.firstChild<AndroidSpaceship>()!;
androidSpaceship.bloc.stream.listen((state) {
final listenWhen = state == AndroidSpaceshipState.withBonus;
if (!listenWhen) return;
bloc.add(const BonusActivated(GameBonus.androidSpaceship));
androidSpaceship.bloc.onBonusAwarded();
});
Future<void> onLoad() async {
await super.onLoad();
await add(
FlameBlocListener<AndroidSpaceshipCubit, AndroidSpaceshipState>(
listenWhen: (_, state) => state == AndroidSpaceshipState.withBonus,
onNewState: (state) {
readBloc<GameBloc, GameState>().add(
const BonusActivated(GameBonus.androidSpaceship),
);
readBloc<AndroidSpaceshipCubit, AndroidSpaceshipState>()
.onBonusAwarded();
},
),
);
}
}

@ -5,27 +5,37 @@ import 'package:flutter/material.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' hide Assets;
import 'package:platform_helper/platform_helper.dart';
/// {@template backbox}
/// The [Backbox] of the pinball machine.
/// {@endtemplate}
class Backbox extends PositionComponent with ZIndex {
class Backbox extends PositionComponent with ZIndex, HasGameRef {
/// {@macro backbox}
Backbox({
required LeaderboardRepository leaderboardRepository,
}) : _bloc = BackboxBloc(leaderboardRepository: leaderboardRepository);
required List<LeaderboardEntryData>? entries,
}) : _bloc = BackboxBloc(
leaderboardRepository: leaderboardRepository,
initialEntries: entries,
),
_platformHelper = PlatformHelper();
/// {@macro backbox}
@visibleForTesting
Backbox.test({
required BackboxBloc bloc,
}) : _bloc = bloc;
required PlatformHelper platformHelper,
}) : _bloc = bloc,
_platformHelper = platformHelper;
late final Component _display;
final BackboxBloc _bloc;
final PlatformHelper _platformHelper;
late StreamSubscription<BackboxState> _subscription;
@override
@ -34,8 +44,6 @@ class Backbox extends PositionComponent with ZIndex {
anchor = Anchor.bottomCenter;
zIndex = ZIndexes.backbox;
_bloc.add(LeaderboardRequested());
await add(_BackboxSpriteComponent());
await add(_display = Component());
_build(_bloc.state);
@ -58,6 +66,9 @@ class Backbox extends PositionComponent with ZIndex {
} else if (state is LeaderboardSuccessState) {
_display.add(LeaderboardDisplay(entries: state.entries));
} else if (state is InitialsFormState) {
if (_platformHelper.isMobile) {
gameRef.overlays.add(PinballGame.mobileControlsOverlay);
}
_display.add(
InitialsInputDisplay(
score: state.score,

@ -14,8 +14,13 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
/// {@macro backbox_bloc}
BackboxBloc({
required LeaderboardRepository leaderboardRepository,
required List<LeaderboardEntryData>? initialEntries,
}) : _leaderboardRepository = leaderboardRepository,
super(LoadingState()) {
super(
initialEntries != null
? LeaderboardSuccessState(entries: initialEntries)
: LeaderboardFailureState(),
) {
on<PlayerInitialsRequested>(_onPlayerInitialsRequested);
on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted);
on<ShareScoreRequested>(_onScoreShareRequested);

@ -1,6 +1,5 @@
import 'package:flame/components.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -40,7 +39,7 @@ class _BottomGroupSide extends Component {
final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? -0.45 : -6.8;
final flipper = ControlledFlipper(
final flipper = Flipper(
side: _side,
)..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side)

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart';
export 'bottom_group.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart';
export 'drain/drain.dart';

@ -1,15 +1,15 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Bonus obtained at the [FlutterForest].
///
/// When all [DashNestBumper]s are hit at least once three times, the [Signpost]
/// When all [DashBumper]s are hit at least once three times, the [Signpost]
/// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
/// is awarded, and the [DashBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component
with
ParentIsA<FlutterForest>,
@ -19,15 +19,14 @@ class FlutterForestBonusBehavior extends Component
void onMount() {
super.onMount();
final bumpers = parent.children.whereType<DashNestBumper>();
final bumpers = parent.children.whereType<DashBumper>();
final signpost = parent.firstChild<Signpost>()!;
final animatronic = parent.firstChild<DashAnimatronic>()!;
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
for (final bumper in bumpers) {
bumper.bloc.stream.listen((state) {
final activatedAllBumpers = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active,
(bumper) => bumper.bloc.state == DashBumperState.active,
);
if (activatedAllBumpers) {
@ -38,15 +37,7 @@ class FlutterForestBonusBehavior extends Component
if (signpost.bloc.isFullyProgressed()) {
bloc.add(const BonusActivated(GameBonus.dashNest));
final characterTheme =
readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme;
canvas.add(
Ball(assetPath: characterTheme.ball.keyName)
..initialPosition = Vector2(29.2, -24.5)
..zIndex = ZIndexes.ballOnBoard,
);
add(BonusBallSpawningBehavior());
animatronic.playing = true;
signpost.bloc.onProgressed();
}

@ -9,7 +9,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template flutter_forest}
/// Area positioned at the top right of the board where the [Ball] can bounce
/// off [DashNestBumper]s.
/// off [DashBumper]s.
/// {@endtemplate}
class FlutterForest extends Component with ZIndex {
/// {@macro flutter_forest}
@ -22,19 +22,19 @@ class FlutterForest extends Component with ZIndex {
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(7.95, -58.35),
DashNestBumper.main(
DashBumper.main(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a(
DashBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b(
DashBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),

@ -3,6 +3,7 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Listens to the [GameBloc] and updates the game accordingly.
@ -20,6 +21,11 @@ class GameBlocStatusListener extends Component
break;
case GameStatus.playing:
readProvider<PinballPlayer>().play(PinballAudio.backgroundMusic);
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_addFlipperKeyControls);
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
case GameStatus.gameOver:
@ -30,7 +36,23 @@ class GameBlocStatusListener extends Component
.state
.characterTheme,
);
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_removeFlipperKeyControls);
break;
}
}
void _addFlipperKeyControls(Flipper flipper) {
flipper
..add(FlipperKeyControllingBehavior())
..moveDown();
}
void _removeFlipperKeyControls(Flipper flipper) => flipper
.descendants()
.whereType<FlipperKeyControllingBehavior>()
.forEach(flipper.remove);
}

@ -1,3 +1,4 @@
import 'package:flame/extensions.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_theme/pinball_theme.dart' hide Assets;
@ -5,7 +6,7 @@ import 'package:pinball_theme/pinball_theme.dart' hide Assets;
/// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame {
/// Returns a list of assets to be loaded
List<Future> preLoadAssets() {
List<Future<Image>> preLoadAssets() {
const dashTheme = DashTheme();
const sparkyTheme = SparkyTheme();
const androidTheme = AndroidTheme();

@ -37,8 +37,8 @@ class PinballGame extends PinballForge2DGame
/// Identifier of the play button overlay
static const playButtonOverlay = 'play_button';
/// Identifier of the replay button overlay
static const replayButtonOverlay = 'replay_button';
/// Identifier of the mobile controls overlay
static const mobileControlsOverlay = 'mobile_controls';
@override
Color backgroundColor() => Colors.transparent;
@ -55,6 +55,18 @@ class PinballGame extends PinballForge2DGame
final GameBloc _gameBloc;
List<LeaderboardEntryData>? _entries;
Future<void> preFetchLeaderboard() async {
try {
_entries = await leaderboardRepository.fetchTop10Leaderboard();
} catch (_) {
// An initial null leaderboard means that we couldn't fetch
// the entries for the [Backbox] and it will show the relevant display.
_entries = null;
}
}
@override
Future<void> onLoad() async {
await add(
@ -93,6 +105,7 @@ class PinballGame extends PinballForge2DGame
Boundaries(),
Backbox(
leaderboardRepository: leaderboardRepository,
entries: _entries,
),
GoogleWord(position: Vector2(-4.45, 1.8)),
Multipliers(),

@ -21,12 +21,6 @@ class PinballGamePage extends StatelessWidget {
final bool isDebugMode;
static Route route({bool isDebugMode = kDebugMode}) {
return MaterialPageRoute<void>(
builder: (_) => PinballGamePage(isDebugMode: isDebugMode),
);
}
@override
Widget build(BuildContext context) {
final characterThemeBloc = context.read<CharacterThemeCubit>();
@ -48,52 +42,39 @@ class PinballGamePage extends StatelessWidget {
l10n: context.l10n,
gameBloc: gameBloc,
);
final loadables = [
...game.preLoadAssets(),
...player.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
];
return BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
child: PinballGameView(game: game),
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: BlocProvider(
create: (_) => AssetsManagerCubit(game, player)..load(),
child: PinballGameView(game),
),
),
);
}
}
class PinballGameView extends StatelessWidget {
const PinballGameView({
Key? key,
required this.game,
}) : super(key: key);
const PinballGameView(this.game, {Key? key}) : super(key: key);
final PinballGame game;
@override
Widget build(BuildContext context) {
final isLoading = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress != 1,
);
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: isLoading
return BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
builder: (context, state) {
return state.isLoading
? const AssetsLoadingPage()
: PinballGameLoadedView(game: game),
),
: PinballGameLoadedView(game);
},
);
}
}
@visibleForTesting
class PinballGameLoadedView extends StatelessWidget {
const PinballGameLoadedView({
Key? key,
required this.game,
}) : super(key: key);
const PinballGameLoadedView(this.game, {Key? key}) : super(key: key);
final PinballGame game;
@ -122,12 +103,12 @@ class PinballGameLoadedView extends StatelessWidget {
child: PlayButtonOverlay(),
);
},
PinballGame.replayButtonOverlay: (context, game) {
return const Positioned(
bottom: 20,
right: 0,
PinballGame.mobileControlsOverlay: (context, game) {
return Positioned(
bottom: 0,
left: 0,
child: ReplayButtonOverlay(),
right: 0,
child: MobileControls(game: game),
);
},
},

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template mobile_controls}
/// Widget with the controls used to enable the user initials input on mobile.
/// {@endtemplate}
class MobileControls extends StatelessWidget {
/// {@macro mobile_controls}
const MobileControls({
Key? key,
required this.game,
}) : super(key: key);
/// Game instance
final PinballGame game;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
MobileDpad(
onTapUp: () => game.triggerVirtualKeyUp(LogicalKeyboardKey.arrowUp),
onTapDown: () => game.triggerVirtualKeyUp(
LogicalKeyboardKey.arrowDown,
),
onTapLeft: () => game.triggerVirtualKeyUp(
LogicalKeyboardKey.arrowLeft,
),
onTapRight: () => game.triggerVirtualKeyUp(
LogicalKeyboardKey.arrowRight,
),
),
PinballButton(
text: l10n.enter,
onTap: () => game.triggerVirtualKeyUp(LogicalKeyboardKey.enter),
),
],
);
}
}

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template mobile_dpad}
/// Widget rendering 4 directional input arrows.
/// {@endtemplate}
class MobileDpad extends StatelessWidget {
/// {@template mobile_dpad}
const MobileDpad({
Key? key,
required this.onTapUp,
required this.onTapDown,
required this.onTapLeft,
required this.onTapRight,
}) : super(key: key);
static const _size = 180.0;
/// Called when dpad up is pressed
final VoidCallback onTapUp;
/// Called when dpad down is pressed
final VoidCallback onTapDown;
/// Called when dpad left is pressed
final VoidCallback onTapLeft;
/// Called when dpad right is pressed
final VoidCallback onTapRight;
@override
Widget build(BuildContext context) {
return SizedBox(
width: _size,
height: _size,
child: Column(
children: [
Row(
children: [
const Spacer(),
PinballDpadButton(
direction: PinballDpadDirection.up,
onTap: onTapUp,
),
const Spacer(),
],
),
Row(
children: [
PinballDpadButton(
direction: PinballDpadDirection.left,
onTap: onTapLeft,
),
const Spacer(),
PinballDpadButton(
direction: PinballDpadDirection.right,
onTap: onTapRight,
),
],
),
Row(
children: [
const Spacer(),
PinballDpadButton(
direction: PinballDpadDirection.down,
onTap: onTapDown,
),
const Spacer(),
],
),
],
),
);
}
}

@ -1,5 +1,7 @@
export 'bonus_animation.dart';
export 'game_hud.dart';
export 'mobile_controls.dart';
export 'mobile_dpad.dart';
export 'play_button_overlay.dart';
export 'replay_button_overlay.dart';
export 'round_count_display.dart';

@ -15,6 +15,8 @@ class $AssetsImagesGen {
$AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesLoadingGameGen get loadingGame =>
const $AssetsImagesLoadingGameGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
}
@ -62,6 +64,14 @@ class $AssetsImagesLinkBoxGen {
const AssetGenImage('assets/images/link_box/info_icon.png');
}
class $AssetsImagesLoadingGameGen {
const $AssetsImagesLoadingGameGen();
/// File path: assets/images/loading_game/io_pinball.png
AssetGenImage get ioPinball =>
const AssetGenImage('assets/images/loading_game/io_pinball.png');
}
class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen();

@ -240,7 +240,7 @@ class _DesktopFlipperControls extends StatelessWidget {
children: [
Text(
l10n.flipperControls,
style: Theme.of(context).textTheme.subtitle2,
style: Theme.of(context).textTheme.headline4,
),
const SizedBox(height: 10),
Column(

@ -183,5 +183,9 @@
"openSourceCode": "open source code.",
"@openSourceCode": {
"description": "Text shown on description of info screen"
},
"enter": "Enter",
"@enter": {
"description": "Text shown on the mobile controls enter button"
}
}

@ -55,8 +55,6 @@ class _LinkBoxHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final indent = MediaQuery.of(context).size.width / 5;
return Column(
children: [
Text(
@ -68,11 +66,9 @@ class _LinkBoxHeader extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Divider(
color: PinballColors.white,
endIndent: indent,
indent: indent,
thickness: 2,
const SizedBox(
width: 200,
child: Divider(color: PinballColors.white, thickness: 2),
),
],
);

@ -14,8 +14,11 @@ class $AssetsMusicGen {
class $AssetsSfxGen {
const $AssetsSfxGen();
String get android => 'assets/sfx/android.mp3';
String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3';
String get dash => 'assets/sfx/dash.mp3';
String get dino => 'assets/sfx/dino.mp3';
String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3';
String get google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';

@ -28,6 +28,15 @@ enum PinballAudio {
/// Sparky
sparky,
/// Android
android,
/// Dino
dino,
/// Dash
dash,
}
/// Defines the contract of the creation of an [AudioPool].
@ -169,6 +178,21 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio,
path: Assets.sfx.sparky,
),
PinballAudio.dino: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.dino,
),
PinballAudio.dash: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.dash,
),
PinballAudio.android: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.android,
),
PinballAudio.launcher: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,

@ -145,6 +145,18 @@ void main() {
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/sparky.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/dino.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/android.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/dash.mp3'),
).called(1);
verify(
() => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
@ -239,6 +251,42 @@ void main() {
});
});
group('dino', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.dino);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.dino}'),
).called(1);
});
});
group('android', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.android);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.android}'),
).called(1);
});
});
group('dash', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.dash);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.dash}'),
).called(1);
});
});
group('launcher', () {
test('plays the correct file', () async {
await Future.wait(player.load());

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

@ -11,10 +11,8 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_spaceship_cubit.dart';
class AndroidSpaceship extends Component {
AndroidSpaceship({
required Vector2 position,
}) : bloc = AndroidSpaceshipCubit(),
super(
AndroidSpaceship({required Vector2 position})
: super(
children: [
_SpaceshipSaucer()..initialPosition = position,
_SpaceshipSaucerSpriteAnimationComponent()..position = position,
@ -38,17 +36,8 @@ class AndroidSpaceship extends Component {
/// This can be used for testing [AndroidSpaceship]'s behaviors in isolation.
@visibleForTesting
AndroidSpaceship.test({
required this.bloc,
Iterable<Component>? children,
}) : super(children: children);
final AndroidSpaceshipCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
}
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {

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

@ -0,0 +1,22 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ball_impulsing_behavior}
/// Impulses the [Ball] in a given direction.
/// {@endtemplate}
class BallImpulsingBehavior extends Component with ParentIsA<Ball> {
/// {@macro ball_impulsing_behavior}
BallImpulsingBehavior({
required Vector2 impulse,
}) : _impulse = impulse;
final Vector2 _impulse;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.body.linearVelocity = _impulse;
shouldRemove = true;
}
}

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

@ -10,12 +10,12 @@ export 'boundaries.dart';
export 'camera_zoom.dart';
export 'chrome_dino/chrome_dino.dart';
export 'dash_animatronic.dart';
export 'dash_nest_bumper/dash_nest_bumper.dart';
export 'dash_bumper/dash_bumper.dart';
export 'dino_walls.dart';
export 'error_component.dart';
export 'fire_effect.dart';
export 'flapper/flapper.dart';
export 'flipper.dart';
export 'flipper/flipper.dart';
export 'google_letter/google_letter.dart';
export 'initial_position.dart';
export 'joint_anchor.dart';
@ -26,7 +26,7 @@ export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart';
export 'rocket.dart';
export 'score_component.dart';
export 'score_component/score_component.dart';
export 'shapes/shapes.dart';
export 'signpost/signpost.dart';
export 'skill_shot/skill_shot.dart';

@ -2,7 +2,7 @@ import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template dash_animatronic}
/// Animated Dash that sits on top of the [DashNestBumper.main].
/// Animated Dash that sits on top of the [DashBumper.main].
/// {@endtemplate}
class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
/// {@macro dash_animatronic}

@ -2,8 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class DashNestBumperBallContactBehavior
extends ContactBehavior<DashNestBumper> {
class DashBumperBallContactBehavior extends ContactBehavior<DashBumper> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);

@ -0,0 +1,17 @@
import 'package:bloc/bloc.dart';
part 'dash_bumper_state.dart';
class DashBumperCubit extends Cubit<DashBumperState> {
DashBumperCubit() : super(DashBumperState.inactive);
/// Event added when the bumper contacts with a ball.
void onBallContacted() {
emit(DashBumperState.active);
}
/// Event added when the bumper should return to its initial configuration.
void onReset() {
emit(DashBumperState.inactive);
}
}

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

@ -5,17 +5,17 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/bumping_behavior.dart';
import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart';
import 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/dash_nest_bumper_cubit.dart';
export 'cubit/dash_bumper_cubit.dart';
/// {@template dash_nest_bumper}
/// Bumper with a nest appearance.
/// {@template dash_bumper}
/// Bumper for the flutter forest.
/// {@endtemplate}
class DashNestBumper extends BodyComponent with InitialPosition {
/// {@macro dash_nest_bumper}
DashNestBumper._({
class DashBumper extends BodyComponent with InitialPosition {
/// {@macro dash_bumper}
DashBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
@ -28,19 +28,22 @@ class DashNestBumper extends BodyComponent with InitialPosition {
super(
renderBody: false,
children: [
_DashNestBumperSpriteGroupComponent(
_DashBumperSpriteGroupComponent(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
position: spritePosition,
current: bloc.state,
),
DashNestBumperBallContactBehavior(),
DashBumperBallContactBehavior(),
...?children,
],
);
/// {@macro dash_nest_bumper}
DashNestBumper.main({
/// {@macro dash_bumper}
///
/// [DashBumper.main], usually positioned with a [DashAnimatronic] on top of
/// it.
DashBumper.main({
Iterable<Component>? children,
}) : this._(
majorRadius: 5.1,
@ -48,15 +51,18 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spritePosition: Vector2(0, -0.3),
bloc: DashNestBumperCubit(),
bloc: DashBumperCubit(),
children: [
...?children,
BumpingBehavior(strength: 20),
],
);
/// {@macro dash_nest_bumper}
DashNestBumper.a({
/// {@macro dash_bumper}
///
/// [DashBumper.a] is positioned at the right side of the [DashBumper.main] in
/// the flutter forest.
DashBumper.a({
Iterable<Component>? children,
}) : this._(
majorRadius: 3,
@ -64,15 +70,18 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spritePosition: Vector2(0.3, -1.3),
bloc: DashNestBumperCubit(),
bloc: DashBumperCubit(),
children: [
...?children,
BumpingBehavior(strength: 20),
],
);
/// {@macro dash_nest_bumper}
DashNestBumper.b({
/// {@macro dash_bumper}
///
/// [DashBumper.b] is positioned at the left side of the [DashBumper.main] in
/// the flutter forest.
DashBumper.b({
Iterable<Component>? children,
}) : this._(
majorRadius: 3.1,
@ -80,25 +89,26 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spritePosition: Vector2(0.4, -1.2),
bloc: DashNestBumperCubit(),
bloc: DashBumperCubit(),
children: [
...?children,
BumpingBehavior(strength: 20),
],
);
/// Creates an [DashNestBumper] without any children.
/// Creates a [DashBumper] without any children.
///
/// This can be used for testing [DashNestBumper]'s behaviors in isolation.
/// This can be used for testing [DashBumper]'s behaviors in isolation.
@visibleForTesting
DashNestBumper.test({required this.bloc})
DashBumper.test({required this.bloc})
: _majorRadius = 3,
_minorRadius = 2.5;
final double _majorRadius;
final double _minorRadius;
final DashNestBumperCubit bloc;
// ignore: public_member_api_docs
final DashBumperCubit bloc;
@override
void onRemove() {
@ -121,14 +131,14 @@ class DashNestBumper extends BodyComponent with InitialPosition {
}
}
class _DashNestBumperSpriteGroupComponent
extends SpriteGroupComponent<DashNestBumperState>
with HasGameRef, ParentIsA<DashNestBumper> {
_DashNestBumperSpriteGroupComponent({
class _DashBumperSpriteGroupComponent
extends SpriteGroupComponent<DashBumperState>
with HasGameRef, ParentIsA<DashBumper> {
_DashBumperSpriteGroupComponent({
required String activeAssetPath,
required String inactiveAssetPath,
required Vector2 position,
required DashNestBumperState current,
required DashBumperState current,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
super(
@ -146,9 +156,9 @@ class _DashNestBumperSpriteGroupComponent
parent.bloc.stream.listen((state) => current = state);
final sprites = {
DashNestBumperState.active:
DashBumperState.active:
Sprite(gameRef.images.fromCache(_activeAssetPath)),
DashNestBumperState.inactive:
DashBumperState.inactive:
Sprite(gameRef.images.fromCache(_inactiveAssetPath)),
};
this.sprites = sprites;

@ -1,17 +0,0 @@
import 'package:bloc/bloc.dart';
part 'dash_nest_bumper_state.dart';
class DashNestBumperCubit extends Cubit<DashNestBumperState> {
DashNestBumperCubit() : super(DashNestBumperState.inactive);
/// Event added when the bumper contacts with a ball.
void onBallContacted() {
emit(DashNestBumperState.active);
}
/// Event added when the bumper should return to its initial configuration.
void onReset() {
emit(DashNestBumperState.inactive);
}
}

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

@ -0,0 +1,2 @@
export 'flipper_jointing_behavior.dart';
export 'flipper_key_controlling_behavior.dart';

@ -0,0 +1,62 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Joints the [Flipper] to allow pivoting around one end.
class FlipperJointingBehavior extends Component
with ParentIsA<Flipper>, HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final anchor = _FlipperAnchor(flipper: parent);
await add(anchor);
final jointDef = _FlipperAnchorRevoluteJointDef(
flipper: parent,
anchor: anchor,
);
parent.world.createJoint(RevoluteJoint(jointDef));
}
}
/// {@template flipper_anchor}
/// [JointAnchor] positioned at the end of a [Flipper].
///
/// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate}
class _FlipperAnchor extends JointAnchor {
/// {@macro flipper_anchor}
_FlipperAnchor({
required Flipper flipper,
}) {
initialPosition = Vector2(
(Flipper.size.x * flipper.side.direction) / 2 -
(1.65 * flipper.side.direction),
-0.15,
);
}
}
/// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve a potivoting
/// motion.
/// {@endtemplate}
class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def}
_FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required _FlipperAnchor anchor,
}) {
initialize(
flipper.body,
anchor.body,
flipper.body.position + anchor.body.position,
);
enableLimit = true;
upperAngle = 0.611;
lowerAngle = -upperAngle;
}
}

@ -1,49 +1,33 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template controlled_flipper}
/// A [Flipper] with a [FlipperController] attached.
/// {@endtemplate}
class ControlledFlipper extends Flipper with Controls<FlipperController> {
/// {@macro controlled_flipper}
ControlledFlipper({
required BoardSide side,
}) : super(side: side) {
controller = FlipperController(this);
}
}
/// {@template flipper_controller}
/// A [ComponentController] that controls a [Flipper]s movement.
/// {@endtemplate}
class FlipperController extends ComponentController<Flipper>
with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro flipper_controller}
FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys,
super(flipper);
/// Allows controlling the [Flipper]'s movement with keyboard input.
class FlipperKeyControllingBehavior extends Component
with KeyboardHandler, ParentIsA<Flipper> {
/// The [LogicalKeyboardKey]s that will control the [Flipper].
///
/// [onKeyEvent] method listens to when one of these keys is pressed.
final List<LogicalKeyboardKey> _keys;
late final List<LogicalKeyboardKey> _keys;
@override
Future<void> onLoad() async {
await super.onLoad();
_keys = parent.side.flipperKeys;
}
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (!bloc.state.status.isPlaying) return true;
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
component.moveUp();
parent.moveUp();
} else if (event is RawKeyUpEvent) {
component.moveDown();
parent.moveDown();
}
return false;

@ -2,8 +2,11 @@ import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'package:pinball_components/pinball_components.dart';
export 'behaviors/behaviors.dart';
/// {@template flipper}
/// A bat, typically found in pairs at the bottom of the board.
///
@ -15,9 +18,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
required this.side,
}) : super(
renderBody: false,
children: [_FlipperSpriteComponent(side: side)],
children: [
_FlipperSpriteComponent(side: side),
FlipperJointingBehavior(),
],
);
/// Creates a [Flipper] without any children.
///
/// This can be used for testing [Flipper]'s behaviors in isolation.
@visibleForTesting
Flipper.test({required this.side});
/// The size of the [Flipper].
static final size = Vector2(13.5, 4.3);
@ -44,19 +56,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
body.linearVelocity = Vector2(0, -_speed);
}
/// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion.
Future<void> _anchorToJoint() async {
final anchor = _FlipperAnchor(flipper: this);
await add(anchor);
final jointDef = _FlipperAnchorRevoluteJointDef(
flipper: this,
anchor: anchor,
);
final joint = _FlipperJoint(jointDef);
world.createJoint(joint);
}
List<FixtureDef> _createFixtureDefs() {
final direction = side.direction;
@ -73,7 +72,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
assetShadow,
0,
);
final bigCircleFixtureDef = FixtureDef(bigCircleShape);
final smallCircleShape = CircleShape()..radius = size.y * 0.23;
smallCircleShape.position.setValues(
@ -82,7 +80,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
assetShadow,
0,
);
final smallCircleFixtureDef = FixtureDef(smallCircleShape);
final trapeziumVertices = side.isLeft
? [
@ -98,26 +95,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
];
final trapezium = PolygonShape()..set(trapeziumVertices);
final trapeziumFixtureDef = FixtureDef(
trapezium,
density: 50,
friction: .1,
);
return [
bigCircleFixtureDef,
smallCircleFixtureDef,
trapeziumFixtureDef,
FixtureDef(bigCircleShape),
FixtureDef(smallCircleShape),
FixtureDef(
trapezium,
density: 50,
friction: .1,
),
];
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
}
@override
Body createBody() {
final bodyDef = BodyDef(
@ -131,15 +120,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
return body;
}
@override
void onMount() {
super.onMount();
gameRef.ready().whenComplete(
() => body.joints.whereType<_FlipperJoint>().first.unlock(),
);
}
}
class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
@ -163,73 +143,3 @@ class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
size = sprite.originalSize / 10;
}
}
/// {@template flipper_anchor}
/// [JointAnchor] positioned at the end of a [Flipper].
///
/// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate}
class _FlipperAnchor extends JointAnchor {
/// {@macro flipper_anchor}
_FlipperAnchor({
required Flipper flipper,
}) {
initialPosition = Vector2(
(Flipper.size.x * flipper.side.direction) / 2 -
(1.65 * flipper.side.direction),
-0.15,
);
}
}
/// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion.
/// {@endtemplate}
class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def}
_FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required _FlipperAnchor anchor,
}) : side = flipper.side {
enableLimit = true;
initialize(
flipper.body,
anchor.body,
flipper.body.position + anchor.body.position,
);
}
final BoardSide side;
}
/// {@template flipper_joint}
/// [RevoluteJoint] that controls the arc motion of a [Flipper].
/// {@endtemplate}
class _FlipperJoint extends RevoluteJoint {
/// {@macro flipper_joint}
_FlipperJoint(_FlipperAnchorRevoluteJointDef def)
: side = def.side,
super(def) {
lock();
}
/// Half the angle of the arc motion.
static const _halfSweepingAngle = 0.611;
final BoardSide side;
/// Locks the [Flipper] to its resting position.
///
/// The joint is locked when initialized in order to force the [Flipper]
/// at its resting position.
void lock() {
final angle = _halfSweepingAngle * side.direction;
setLimits(angle, angle);
}
/// Unlocks the [Flipper] from its resting position.
void unlock() {
const angle = _halfSweepingAngle;
setLimits(-angle, angle);
}
}

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Scales a [ScoreComponent] according to its position on the board.
class ScoreComponentScalingBehavior extends Component
with ParentIsA<SpriteComponent> {
@override
void update(double dt) {
super.update(dt);
final boardHeight = BoardDimensions.bounds.height;
const maxShrinkValue = 0.83;
final augmentedPosition = parent.position.y * 3;
final standardizedYPosition = augmentedPosition + (boardHeight / 2);
final scaleFactor = maxShrinkValue +
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
parent.scale.setValues(
scaleFactor,
scaleFactor,
);
}
}

@ -2,7 +2,9 @@ import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/score_component/behaviors/score_component_scaling_behavior.dart';
import 'package:pinball_flame/pinball_flame.dart';
enum Points {
@ -26,10 +28,25 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
super(
position: position,
anchor: Anchor.center,
children: [ScoreComponentScalingBehavior()],
) {
zIndex = ZIndexes.score;
}
/// Creates a [ScoreComponent] without any children.
///
/// This can be used for testing [ScoreComponent]'s behaviors in isolation.
@visibleForTesting
ScoreComponent.test({
required this.points,
required Vector2 position,
required EffectController effectController,
}) : _effectController = effectController,
super(
position: position,
anchor: Anchor.center,
);
late Points points;
late final Effect _effect;

@ -9,7 +9,7 @@ export 'cubit/signpost_cubit.dart';
/// {@template signpost}
/// A sign, found in the Flutter Forest.
///
/// Lights up a new sign whenever all three [DashNestBumper]s are hit.
/// Lights up a new sign whenever all three [DashBumper]s are hit.
/// {@endtemplate}
class Signpost extends BodyComponent with InitialPosition {
/// {@macro signpost}

@ -9,7 +9,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// the [SpaceshipRamp].
/// {@endtemplate}
class RampBallAscendingContactBehavior
extends ContactBehavior<RampScoringSensor> {
extends ContactBehavior<SpaceshipRampBoardOpening> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);

@ -27,11 +27,6 @@ class SpaceshipRamp extends Component {
required this.bloc,
}) : super(
children: [
RampScoringSensor(
children: [
RampBallAscendingContactBehavior(),
],
)..initialPosition = Vector2(1.7, -20.4),
_SpaceshipRampOpening(
outsideLayer: Layer.spaceship,
outsidePriority: ZIndexes.ballOnSpaceship,
@ -40,9 +35,9 @@ class SpaceshipRamp extends Component {
..initialPosition = Vector2(-13.7, -18.6)
..layer = Layer.spaceshipEntranceRamp,
_SpaceshipRampBackground(),
_SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5),
SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5),
_SpaceshipRampForegroundRailing(),
_SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5),
SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5),
_SpaceshipRampBackgroundRailingSpriteComponent(),
SpaceshipRampArrowSpriteComponent(
current: bloc.state.hits,
@ -246,18 +241,24 @@ extension on SpaceshipRampArrowSpriteState {
}
}
class _SpaceshipRampBoardOpening extends BodyComponent
with Layered, ZIndex, InitialPosition {
_SpaceshipRampBoardOpening()
class SpaceshipRampBoardOpening extends BodyComponent
with Layered, ZIndex, InitialPosition, ParentIsA<SpaceshipRamp> {
SpaceshipRampBoardOpening()
: super(
renderBody: false,
children: [
_SpaceshipRampBoardOpeningSpriteComponent(),
RampBallAscendingContactBehavior()..applyTo(['inside']),
LayerContactBehavior(layer: Layer.spaceshipEntranceRamp)
..applyTo(['inside']),
LayerContactBehavior(layer: Layer.board)..applyTo(['outside']),
ZIndexContactBehavior(zIndex: ZIndexes.ballOnBoard)
..applyTo(['outside']),
LayerContactBehavior(
layer: Layer.board,
onBegin: false,
)..applyTo(['outside']),
ZIndexContactBehavior(
zIndex: ZIndexes.ballOnBoard,
onBegin: false,
)..applyTo(['outside']),
ZIndexContactBehavior(zIndex: ZIndexes.ballOnSpaceshipRamp)
..applyTo(['middle', 'inside']),
],
@ -266,6 +267,13 @@ class _SpaceshipRampBoardOpening extends BodyComponent
layer = Layer.opening;
}
/// Creates a [SpaceshipRampBoardOpening] without any children.
///
/// This can be used for testing [SpaceshipRampBoardOpening]'s behaviors in
/// isolation.
@visibleForTesting
SpaceshipRampBoardOpening.test();
List<FixtureDef> _createFixtureDefs() {
final topEdge = EdgeShape()
..set(
@ -426,9 +434,19 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
}
}
class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition {
_SpaceshipRampBase() : super(renderBody: false) {
layer = Layer.board;
@visibleForTesting
class SpaceshipRampBase extends BodyComponent
with InitialPosition, ContactCallbacks {
SpaceshipRampBase() : super(renderBody: false);
@override
void preSolve(Object other, Contact contact, Manifold oldManifold) {
super.preSolve(other, contact, oldManifold);
if (other is! Layered) return;
// Although, the Layer should already be taking care of the contact
// filtering, this is to ensure the ball doesn't collide with the ramp base
// when the filtering is calculated on different time steps.
contact.setEnabled(other.layer == Layer.board);
}
@override
@ -441,7 +459,7 @@ class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition {
Vector2(4.1, 1.5),
],
);
final bodyDef = BodyDef(position: initialPosition);
final bodyDef = BodyDef(position: initialPosition, userData: this);
return world.createBody(bodyDef)..createFixtureFromShape(shape);
}
}
@ -480,46 +498,3 @@ class _SpaceshipRampOpening extends LayerSensor {
);
}
}
/// {@template ramp_scoring_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SpaceshipRamp].
/// {@endtemplate}
class RampScoringSensor extends BodyComponent
with ParentIsA<SpaceshipRamp>, InitialPosition, Layered {
/// {@macro ramp_scoring_sensor}
RampScoringSensor({
Iterable<Component>? children,
}) : super(
children: children,
renderBody: false,
) {
layer = Layer.spaceshipEntranceRamp;
}
/// Creates a [RampScoringSensor] without any children.
@visibleForTesting
RampScoringSensor.test();
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
2.6,
.5,
initialPosition,
-5 * math.pi / 180,
);
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -11,7 +11,7 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/sparky_bumper_cubit.dart';
/// {@template sparky_bumper}
/// Bumper for Sparky area.
/// Bumper for the Sparky Scorch.
/// {@endtemplate}
class SparkyBumper extends BodyComponent with InitialPosition, ZIndex {
/// {@macro sparky_bumper}

@ -65,7 +65,7 @@ class SparkyComputer extends BodyComponent {
..setAsBox(
1,
0.1,
Vector2(-13.2, -49.9),
Vector2(-13.1, -49.7),
-0.18,
);

@ -106,7 +106,7 @@ abstract class ZIndexes {
// Score
static const score = _above + spaceshipRampForegroundRailing;
static const score = _above + sparkyAnimatronic;
// Debug information

@ -1,6 +1,4 @@
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
@ -23,16 +21,6 @@ class FlipperGame extends BallGame with KeyboardEvents {
- Press right arrow key or "D" to move the right flipper.
''';
static const _leftFlipperKeys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
];
static const _rightFlipperKeys = [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
];
late Flipper leftFlipper;
late Flipper rightFlipper;
@ -50,32 +38,4 @@ class FlipperGame extends BallGame with KeyboardEvents {
await traceAllBodies();
}
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
final movedLeftFlipper = _leftFlipperKeys.contains(event.logicalKey);
if (movedLeftFlipper) {
if (event is RawKeyDownEvent) {
leftFlipper.moveUp();
} else if (event is RawKeyUpEvent) {
leftFlipper.moveDown();
}
}
final movedRightFlipper = _rightFlipperKeys.contains(event.logicalKey);
if (movedRightFlipper) {
if (event is RawKeyDownEvent) {
rightFlipper.moveUp();
} else if (event is RawKeyUpEvent) {
rightFlipper.moveDown();
}
}
return movedLeftFlipper || movedRightFlipper
? KeyEventResult.handled
: KeyEventResult.ignored;
}
}

@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SmallDashNestBumperAGame extends BallGame {
SmallDashNestBumperAGame()
class DashBumperAGame extends BallGame {
DashBumperAGame()
: super(
imagesFileNames: [
Assets.images.dash.bumper.a.active.keyName,
@ -14,7 +14,7 @@ class SmallDashNestBumperAGame extends BallGame {
);
static const description = '''
Shows how a SmallDashNestBumper ("a") is rendered.
Shows how the "a" DashBumper is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@ -24,7 +24,7 @@ class SmallDashNestBumperAGame extends BallGame {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(DashNestBumper.a()..priority = 1);
await add(DashBumper.a()..priority = 1);
await traceAllBodies();
}
}

@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SmallDashNestBumperBGame extends BallGame {
SmallDashNestBumperBGame()
class DashBumperBGame extends BallGame {
DashBumperBGame()
: super(
imagesFileNames: [
Assets.images.dash.bumper.b.active.keyName,
@ -14,7 +14,7 @@ class SmallDashNestBumperBGame extends BallGame {
);
static const description = '''
Shows how a SmallDashNestBumper ("b") is rendered.
Shows how the "b" DashBumper is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@ -24,7 +24,7 @@ class SmallDashNestBumperBGame extends BallGame {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(DashNestBumper.b()..priority = 1);
await add(DashBumper.b()..priority = 1);
await traceAllBodies();
}
}

@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class BigDashNestBumperGame extends BallGame {
BigDashNestBumperGame()
class DashBumperMainGame extends BallGame {
DashBumperMainGame()
: super(
imagesFileNames: [
Assets.images.dash.bumper.main.active.keyName,
@ -14,7 +14,7 @@ class BigDashNestBumperGame extends BallGame {
);
static const description = '''
Shows how a BigDashNestBumper is rendered.
Shows how the "main" DashBumper is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@ -25,7 +25,7 @@ class BigDashNestBumperGame extends BallGame {
camera.followVector2(Vector2.zero());
await add(
DashNestBumper.main()..priority = 1,
DashBumper.main()..priority = 1,
);
await traceAllBodies();
}

@ -1,9 +1,9 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/flutter_forest/big_dash_nest_bumper_game.dart';
import 'package:sandbox/stories/flutter_forest/dash_bumper_a_game.dart';
import 'package:sandbox/stories/flutter_forest/dash_bumper_b_game.dart';
import 'package:sandbox/stories/flutter_forest/dash_bumper_main_game.dart';
import 'package:sandbox/stories/flutter_forest/signpost_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart';
void addFlutterForestStories(Dashbook dashbook) {
dashbook.storiesOf('Flutter Forest')
@ -13,18 +13,18 @@ void addFlutterForestStories(Dashbook dashbook) {
gameBuilder: (_) => SignpostGame(),
)
..addGame(
title: 'Big Dash Nest Bumper',
description: BigDashNestBumperGame.description,
gameBuilder: (_) => BigDashNestBumperGame(),
title: 'Main Dash Bumper',
description: DashBumperMainGame.description,
gameBuilder: (_) => DashBumperMainGame(),
)
..addGame(
title: 'Small Dash Nest Bumper A',
description: SmallDashNestBumperAGame.description,
gameBuilder: (_) => SmallDashNestBumperAGame(),
title: 'Dash Bumper A',
description: DashBumperAGame.description,
gameBuilder: (_) => DashBumperAGame(),
)
..addGame(
title: 'Small Dash Nest Bumper B',
description: SmallDashNestBumperBGame.description,
gameBuilder: (_) => SmallDashNestBumperBGame(),
title: 'Dash Bumper B',
description: DashBumperBGame.description,
gameBuilder: (_) => DashBumperBGame(),
);
}

@ -1,7 +1,7 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@ -21,9 +21,18 @@ void main() {
Assets.images.android.spaceship.lightBeam.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
late AndroidSpaceshipCubit bloc;
setUp(() {
bloc = _MockAndroidSpaceshipCubit();
});
flameTester.test('loads correctly', (game) async {
final component = AndroidSpaceship(position: Vector2.zero());
final component =
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>.value(
value: bloc,
children: [AndroidSpaceship(position: Vector2.zero())],
);
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
@ -33,7 +42,13 @@ void main() {
setUp: (game, tester) async {
await game.images.loadAll(assets);
final canvas = ZCanvasComponent(
children: [AndroidSpaceship(position: Vector2.zero())],
children: [
FlameBlocProvider<AndroidSpaceshipCubit,
AndroidSpaceshipState>.value(
value: bloc,
children: [AndroidSpaceship(position: Vector2.zero())],
),
],
);
await game.ensureAdd(canvas);
game.camera.followVector2(Vector2.zero());
@ -70,28 +85,16 @@ void main() {
},
);
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockAndroidSpaceshipCubit();
whenListen(
bloc,
const Stream<AndroidSpaceshipState>.empty(),
initialState: AndroidSpaceshipState.withoutBonus,
);
when(bloc.close).thenAnswer((_) async {});
final androidSpaceship = AndroidSpaceship.test(bloc: bloc);
await game.ensureAdd(androidSpaceship);
game.remove(androidSpaceship);
await game.ready();
verify(bloc.close).called(1);
});
flameTester.test(
'AndroidSpaceshipEntrance has an '
'AndroidSpaceshipEntranceBallContactBehavior', (game) async {
final androidSpaceship = AndroidSpaceship(position: Vector2.zero());
await game.ensureAdd(androidSpaceship);
final provider =
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>.value(
value: bloc,
children: [androidSpaceship],
);
await game.ensureAdd(provider);
final androidSpaceshipEntrance =
androidSpaceship.firstChild<AndroidSpaceshipEntrance>();

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
@ -43,16 +44,19 @@ void main() {
);
final entrance = AndroidSpaceshipEntrance();
final androidSpaceship = AndroidSpaceship.test(
bloc: bloc,
children: [entrance],
final androidSpaceship = FlameBlocProvider<AndroidSpaceshipCubit,
AndroidSpaceshipState>.value(
value: bloc,
children: [
AndroidSpaceship.test(children: [entrance])
],
);
await entrance.add(behavior);
await game.ensureAdd(androidSpaceship);
behavior.beginContact(_MockBall(), _MockContact());
verify(androidSpaceship.bloc.onBallEntered).called(1);
verify(bloc.onBallEntered).called(1);
},
);
},

@ -0,0 +1,53 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group(
'BallImpulsingBehavior',
() {
final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
test('can be instantiated', () {
expect(
BallImpulsingBehavior(impulse: Vector2.zero()),
isA<BallImpulsingBehavior>(),
);
});
flameTester.test(
'impulses the ball with the given velocity when loaded '
'and then removes itself',
(game) async {
final ball = Ball.test();
await game.ensureAdd(ball);
final impulse = Vector2.all(1);
final behavior = BallImpulsingBehavior(impulse: impulse);
await ball.ensureAdd(behavior);
expect(
ball.body.linearVelocity.x,
equals(impulse.x),
);
expect(
ball.body.linearVelocity.y,
equals(impulse.y),
);
expect(
game.descendants().whereType<BallImpulsingBehavior>().isEmpty,
isTrue,
);
},
);
},
);
}

@ -10,8 +10,9 @@ import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
final flameTester = FlameTester(
() => TestGame([theme.Assets.images.dash.ball.keyName]),
);
group('BallScalingBehavior', () {
test('can be instantiated', () {

@ -6,11 +6,11 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart';
import 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
class _MockDashBumperCubit extends Mock implements DashBumperCubit {}
class _MockBall extends Mock implements Ball {}
@ -21,33 +21,33 @@ void main() {
final flameTester = FlameTester(TestGame.new);
group(
'DashNestBumperBallContactBehavior',
'DashBumperBallContactBehavior',
() {
test('can be instantiated', () {
expect(
DashNestBumperBallContactBehavior(),
isA<DashNestBumperBallContactBehavior>(),
DashBumperBallContactBehavior(),
isA<DashBumperBallContactBehavior>(),
);
});
flameTester.test(
'beginContact emits onBallContacted when contacts with a ball',
(game) async {
final behavior = DashNestBumperBallContactBehavior();
final bloc = _MockDashNestBumperCubit();
final behavior = DashBumperBallContactBehavior();
final bloc = _MockDashBumperCubit();
whenListen(
bloc,
const Stream<DashNestBumperState>.empty(),
initialState: DashNestBumperState.active,
const Stream<DashBumperState>.empty(),
initialState: DashBumperState.active,
);
final dashNestBumper = DashNestBumper.test(bloc: bloc);
await dashNestBumper.add(behavior);
await game.ensureAdd(dashNestBumper);
final bumper = DashBumper.test(bloc: bloc);
await bumper.add(behavior);
await game.ensureAdd(bumper);
behavior.beginContact(_MockBall(), _MockContact());
verify(dashNestBumper.bloc.onBallContacted).called(1);
verify(bumper.bloc.onBallContacted).called(1);
},
);
},

@ -4,20 +4,20 @@ import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'DashNestBumperCubit',
'DashBumperCubit',
() {
blocTest<DashNestBumperCubit, DashNestBumperState>(
blocTest<DashBumperCubit, DashBumperState>(
'onBallContacted emits active',
build: DashNestBumperCubit.new,
build: DashBumperCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [DashNestBumperState.active],
expect: () => [DashBumperState.active],
);
blocTest<DashNestBumperCubit, DashNestBumperState>(
blocTest<DashBumperCubit, DashBumperState>(
'onReset emits inactive',
build: DashNestBumperCubit.new,
build: DashBumperCubit.new,
act: (bloc) => bloc.onReset(),
expect: () => [DashNestBumperState.inactive],
expect: () => [DashBumperState.inactive],
);
},
);

@ -0,0 +1,139 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/bumping_behavior.dart';
import 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
class _MockDashBumperCubit extends Mock implements DashBumperCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('DashBumper', () {
final flameTester = FlameTester(
() => TestGame(
[
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
],
),
);
flameTester.test('"main" loads correctly', (game) async {
final bumper = DashBumper.main();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"a" loads correctly', (game) async {
final bumper = DashBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = DashBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockDashBumperCubit();
whenListen(
bloc,
const Stream<DashBumperState>.empty(),
initialState: DashBumperState.inactive,
);
when(bloc.close).thenAnswer((_) async {});
final bumper = DashBumper.test(bloc: bloc);
await game.ensureAdd(bumper);
game.remove(bumper);
await game.ready();
verify(bloc.close).called(1);
});
flameTester.test('adds a bumperBallContactBehavior', (game) async {
final bumper = DashBumper.a();
await game.ensureAdd(bumper);
expect(
bumper.children.whereType<DashBumperBallContactBehavior>().single,
isNotNull,
);
});
group("'main' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final bumper = DashBumper.main(
children: [component],
);
await game.ensureAdd(bumper);
expect(bumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final bumper = DashBumper.main();
await game.ensureAdd(bumper);
expect(
bumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
group("'a' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final bumper = DashBumper.a(
children: [component],
);
await game.ensureAdd(bumper);
expect(bumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final bumper = DashBumper.a();
await game.ensureAdd(bumper);
expect(
bumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
group("'b' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final bumper = DashBumper.b(
children: [component],
);
await game.ensureAdd(bumper);
expect(bumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final bumper = DashBumper.b();
await game.ensureAdd(bumper);
expect(
bumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
});
}

@ -1,137 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/bumping_behavior.dart';
import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('DashNestBumper', () {
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('"main" loads correctly', (game) async {
final bumper = DashNestBumper.main();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"a" loads correctly', (game) async {
final bumper = DashNestBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = DashNestBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockDashNestBumperCubit();
whenListen(
bloc,
const Stream<DashNestBumperState>.empty(),
initialState: DashNestBumperState.inactive,
);
when(bloc.close).thenAnswer((_) async {});
final dashNestBumper = DashNestBumper.test(bloc: bloc);
await game.ensureAdd(dashNestBumper);
game.remove(dashNestBumper);
await game.ready();
verify(bloc.close).called(1);
});
flameTester.test('adds a DashNestBumperBallContactBehavior', (game) async {
final dashNestBumper = DashNestBumper.a();
await game.ensureAdd(dashNestBumper);
expect(
dashNestBumper.children
.whereType<DashNestBumperBallContactBehavior>()
.single,
isNotNull,
);
});
group("'main' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final dashNestBumper = DashNestBumper.main(
children: [component],
);
await game.ensureAdd(dashNestBumper);
expect(dashNestBumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final dashNestBumper = DashNestBumper.main();
await game.ensureAdd(dashNestBumper);
expect(
dashNestBumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
group("'a' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final dashNestBumper = DashNestBumper.a(
children: [component],
);
await game.ensureAdd(dashNestBumper);
expect(dashNestBumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final dashNestBumper = DashNestBumper.a();
await game.ensureAdd(dashNestBumper);
expect(
dashNestBumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
group("'b' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final dashNestBumper = DashNestBumper.b(
children: [component],
);
await game.ensureAdd(dashNestBumper);
expect(dashNestBumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final dashNestBumper = DashNestBumper.b();
await game.ensureAdd(dashNestBumper);
expect(
dashNestBumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
});
}

@ -0,0 +1,38 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/components/components.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('FlipperJointingBehavior', () {
final flameTester = FlameTester(TestGame.new);
test('can be instantiated', () {
expect(
FlipperJointingBehavior(),
isA<FlipperJointingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final behavior = FlipperJointingBehavior();
final parent = Flipper.test(side: BoardSide.left);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
expect(parent.contains(behavior), isTrue);
});
flameTester.test('creates a joint', (game) async {
final behavior = FlipperJointingBehavior();
final parent = Flipper.test(side: BoardSide.left);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
expect(parent.body.joints, isNotEmpty);
});
});
}

@ -0,0 +1,357 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString();
}
}
class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString();
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('FlipperKeyControllingBehavior', () {
final flameTester = FlameTester(TestGame.new);
group(
'onKeyEvent',
() {
late Flipper rightFlipper;
late Flipper leftFlipper;
setUp(() {
rightFlipper = Flipper.test(side: BoardSide.right);
leftFlipper = Flipper.test(side: BoardSide.left);
});
group('on right Flipper', () {
flameTester.test(
'moves upwards when right arrow is pressed',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowRight,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isNegative);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves downwards when right arrow is released',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowRight,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isPositive);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves upwards when D is pressed',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyD,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isNegative);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves downwards when D is released',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyD,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isPositive);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
group("doesn't move when", () {
flameTester.test(
'left arrow is pressed',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowLeft,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'left arrow is released',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowLeft,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'A is pressed',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyA,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'A is released',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyA,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
});
});
group('on left Flipper', () {
flameTester.test(
'moves upwards when left arrow is pressed',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowLeft,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isNegative);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves downwards when left arrow is released',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowLeft,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isPositive);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves upwards when A is pressed',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyA,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isNegative);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves downwards when A is released',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyA,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isPositive);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
group("doesn't move when", () {
flameTester.test(
'right arrow is pressed',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowRight,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'right arrow is released',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowRight,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'D is pressed',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyD,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'D is released',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyD,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
});
});
},
);
});
}

@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../helpers/helpers.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -36,7 +36,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/flipper.png'),
matchesGoldenFile('../golden/flipper.png'),
);
},
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

@ -0,0 +1,74 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/score_component/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
group('ScoreComponentScalingBehavior', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(
() => TestGame([
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
]),
);
test('can be instantiated', () {
expect(
ScoreComponentScalingBehavior(),
isA<ScoreComponentScalingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final parent = ScoreComponent.test(
points: Points.fiveThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
);
final behavior = ScoreComponentScalingBehavior();
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
expect(parent.children, contains(behavior));
});
flameTester.test(
'scales the sprite',
(game) async {
final parent1 = ScoreComponent.test(
points: Points.fiveThousand,
position: Vector2(0, 10),
effectController: EffectController(duration: 1),
);
final parent2 = ScoreComponent.test(
points: Points.fiveThousand,
position: Vector2(0, -10),
effectController: EffectController(duration: 1),
);
await game.ensureAddAll([parent1, parent2]);
await parent1.ensureAdd(ScoreComponentScalingBehavior());
await parent2.ensureAdd(ScoreComponentScalingBehavior());
game.update(1);
expect(
parent1.scale.x,
greaterThan(parent2.scale.x),
);
expect(
parent1.scale.y,
greaterThan(parent2.scale.y),
);
},
);
});
}

@ -5,24 +5,37 @@ import 'package:flame/effects.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/score_component/behaviors/behaviors.dart';
import '../../helpers/helpers.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
final flameTester = FlameTester(
() => TestGame([
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
]),
);
group('ScoreComponent', () {
test('can be instantiated', () {
expect(
ScoreComponent(
points: Points.fiveThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
),
isA<ScoreComponent>(),
);
});
flameTester.testGameWidget(
'loads correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreComponent(
@ -38,13 +51,32 @@ void main() {
},
);
flameTester.test(
'adds a ScoreComponentScalingBehavior',
(game) async {
await game.onLoad();
game.camera.followVector2(Vector2.zero());
final component = ScoreComponent(
points: Points.oneMillion,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
);
await game.ensureAdd(component);
expect(
component.children.whereType<ScoreComponentScalingBehavior>().length,
equals(1),
);
},
);
flameTester.testGameWidget(
'has a movement effect',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.oneMillion,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -63,10 +95,10 @@ void main() {
flameTester.testGameWidget(
'is removed once finished',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.oneMillion,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -83,12 +115,14 @@ void main() {
);
group('renders correctly', () {
const goldensPath = '../golden/score/';
flameTester.testGameWidget(
'5000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.fiveThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -104,7 +138,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/5k.png'),
matchesGoldenFile('${goldensPath}5k.png'),
);
},
);
@ -112,9 +146,9 @@ void main() {
flameTester.testGameWidget(
'20000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.twentyThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -130,7 +164,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/20k.png'),
matchesGoldenFile('${goldensPath}20k.png'),
);
},
);
@ -138,9 +172,9 @@ void main() {
flameTester.testGameWidget(
'200000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.twoHundredThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -156,7 +190,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/200k.png'),
matchesGoldenFile('${goldensPath}200k.png'),
);
},
);
@ -164,9 +198,9 @@ void main() {
flameTester.testGameWidget(
'1000000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.oneMillion,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -182,7 +216,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/1m.png'),
matchesGoldenFile('${goldensPath}1m.png'),
);
},
);

@ -67,16 +67,16 @@ void main() {
initialState: const SpaceshipRampState.initial(),
);
final rampSensor = RampScoringSensor.test();
final parent = SpaceshipRampBoardOpening.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
);
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
await spaceshipRamp.add(rampSensor);
await spaceshipRamp.add(parent);
await game.ensureAddAll([spaceshipRamp, ball]);
await rampSensor.add(behavior);
await parent.add(behavior);
behavior.beginContact(ball, _MockContact());
@ -95,16 +95,16 @@ void main() {
initialState: const SpaceshipRampState.initial(),
);
final rampSensor = RampScoringSensor.test();
final parent = SpaceshipRampBoardOpening.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
);
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
await spaceshipRamp.add(rampSensor);
await spaceshipRamp.add(parent);
await game.ensureAddAll([spaceshipRamp, ball]);
await rampSensor.add(behavior);
await parent.add(behavior);
behavior.beginContact(ball, _MockContact());

@ -2,16 +2,24 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../../helpers/helpers.dart';
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
class _MockManifold extends Mock implements Manifold {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
@ -275,4 +283,71 @@ void main() {
});
});
});
group('SpaceshipRampBase', () {
test('can be instantiated', () {
expect(SpaceshipRampBase(), isA<SpaceshipRampBase>());
});
flameTester.test('can be loaded', (game) async {
final component = SpaceshipRampBase();
await game.ensureAdd(component);
expect(game.children, contains(component));
});
flameTester.test(
'postSolves disables contact when ball is not on Layer.board',
(game) async {
final ball = _MockBall();
final contact = _MockContact();
when(() => ball.layer).thenReturn(Layer.spaceshipEntranceRamp);
final component = SpaceshipRampBase();
await game.ensureAdd(component);
component.preSolve(ball, contact, _MockManifold());
verify(() => contact.setEnabled(false)).called(1);
},
);
flameTester.test(
'postSolves enables contact when ball is on Layer.board',
(game) async {
final ball = _MockBall();
final contact = _MockContact();
when(() => ball.layer).thenReturn(Layer.board);
final component = SpaceshipRampBase();
await game.ensureAdd(component);
component.preSolve(ball, contact, _MockManifold());
verify(() => contact.setEnabled(true)).called(1);
},
);
});
group('SpaceshipRampBoardOpening', () {
test('can be instantiated', () {
expect(SpaceshipRampBoardOpening(), isA<SpaceshipRampBoardOpening>());
});
flameTester.test('can be loaded', (game) async {
final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit());
final component = SpaceshipRampBoardOpening();
await game.ensureAdd(parent);
await parent.ensureAdd(component);
expect(parent.children, contains(component));
});
flameTester.test('adds a RampBallAscendingContactBehavior', (game) async {
final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit());
final component = SpaceshipRampBoardOpening();
await game.ensureAdd(parent);
await parent.ensureAdd(component);
expect(
component.children.whereType<RampBallAscendingContactBehavior>().length,
equals(1),
);
});
});
}

@ -6,15 +6,20 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@endtemplate}
class LayerContactBehavior extends ContactBehavior<BodyComponent> {
/// {@macro layer_contact_behavior}
LayerContactBehavior({required Layer layer}) : _layer = layer;
final Layer _layer;
LayerContactBehavior({
required Layer layer,
bool onBegin = true,
}) {
if (onBegin) {
onBeginContact = (other, _) => _changeLayer(other, layer);
} else {
onEndContact = (other, _) => _changeLayer(other, layer);
}
}
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
void _changeLayer(Object other, Layer layer) {
if (other is! Layered) return;
if (other.layer == _layer) return;
other.layer = _layer;
if (other.layer == layer) return;
other.layer = layer;
}
}

@ -6,15 +6,20 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@endtemplate}
class ZIndexContactBehavior extends ContactBehavior<BodyComponent> {
/// {@macro layer_contact_behavior}
ZIndexContactBehavior({required int zIndex}) : _zIndex = zIndex;
final int _zIndex;
ZIndexContactBehavior({
required int zIndex,
bool onBegin = true,
}) {
if (onBegin) {
onBeginContact = (other, _) => _changeZIndex(other, zIndex);
} else {
onEndContact = (other, _) => _changeZIndex(other, zIndex);
}
}
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
void _changeZIndex(Object other, int zIndex) {
if (other is! ZIndex) return;
if (other.zIndex == _zIndex) return;
other.zIndex = _zIndex;
if (other.zIndex == zIndex) return;
other.zIndex = zIndex;
}
}

@ -1,4 +1,5 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/services.dart';
/// The signature for a key handle function
@ -18,6 +19,17 @@ class KeyboardInputController extends Component with KeyboardHandler {
final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyUp;
final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyDown;
/// Trigger a virtual key up event.
bool onVirtualKeyUp(LogicalKeyboardKey key) {
final handler = _keyUp[key];
if (handler != null) {
return handler();
}
return true;
}
@override
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
final isUp = event is RawKeyUpEvent;
@ -32,3 +44,18 @@ class KeyboardInputController extends Component with KeyboardHandler {
return true;
}
}
/// Add the ability to virtually trigger key events to a [FlameGame]'s
/// [KeyboardInputController].
extension VirtualKeyEvents on FlameGame {
/// Trigger a key up
void triggerVirtualKeyUp(LogicalKeyboardKey key) {
final keyControllers = descendants().whereType<KeyboardInputController>();
for (final controller in keyControllers) {
if (!controller.onVirtualKeyUp(key)) {
break;
}
}
}
}

@ -56,5 +56,23 @@ void main() {
expect(component.layer, newLayer);
});
flameTester.test('endContact changes layer', (game) async {
const oldLayer = Layer.all;
const newLayer = Layer.board;
final behavior = LayerContactBehavior(
layer: newLayer,
onBegin: false,
);
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
final component = _TestLayeredBodyComponent(layer: oldLayer);
behavior.endContact(component, _MockContact());
expect(component.layer, newLayer);
});
});
}

@ -56,5 +56,20 @@ void main() {
expect(component.zIndex, newIndex);
});
flameTester.test('endContact changes zIndex', (game) async {
const oldIndex = 0;
const newIndex = 1;
final behavior = ZIndexContactBehavior(zIndex: newIndex, onBegin: false);
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
final component = _TestZIndexBodyComponent(zIndex: oldIndex);
behavior.endContact(component, _MockContact());
expect(component.zIndex, newIndex);
});
});
}

@ -1,11 +1,36 @@
// ignore_for_file: cascade_invocations, one_member_abstracts
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends FlameGame {
bool pressed = false;
@override
Future<void>? onLoad() async {
await super.onLoad();
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.enter: () {
pressed = true;
return true;
},
LogicalKeyboardKey.escape: () {
return false;
},
},
),
);
}
}
abstract class _KeyCall {
bool onCall();
}
@ -75,4 +100,15 @@ void main() {
},
);
});
group('VirtualKeyEvents', () {
final flameTester = FlameTester(_TestGame.new);
group('onVirtualKeyUp', () {
flameTester.test('triggers the event', (game) async {
await game.ready();
game.triggerVirtualKeyUp(LogicalKeyboardKey.enter);
expect(game.pressed, isTrue);
});
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -3,8 +3,6 @@
/// FlutterGen
/// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
@ -17,7 +15,14 @@ class $AssetsImagesGen {
class $AssetsImagesButtonGen {
const $AssetsImagesButtonGen();
/// File path: assets/images/button/pinball_button.png
AssetGenImage get dpadDown =>
const AssetGenImage('assets/images/button/dpad_down.png');
AssetGenImage get dpadLeft =>
const AssetGenImage('assets/images/button/dpad_left.png');
AssetGenImage get dpadRight =>
const AssetGenImage('assets/images/button/dpad_right.png');
AssetGenImage get dpadUp =>
const AssetGenImage('assets/images/button/dpad_up.png');
AssetGenImage get pinballButton =>
const AssetGenImage('assets/images/button/pinball_button.png');
}
@ -25,7 +30,6 @@ class $AssetsImagesButtonGen {
class $AssetsImagesDialogGen {
const $AssetsImagesDialogGen();
/// File path: assets/images/dialog/background.png
AssetGenImage get background =>
const AssetGenImage('assets/images/dialog/background.png');
}

@ -3,14 +3,9 @@
/// FlutterGen
/// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
class FontFamily {
FontFamily._();
/// Font family: PixeloidMono
static const String pixeloidMono = 'PixeloidMono';
/// Font family: PixeloidSans
static const String pixeloidSans = 'PixeloidSans';
}

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:pinball_ui/gen/gen.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Enum with all possibile directions of a [PinballDpadButton].
enum PinballDpadDirection {
/// Up
up,
/// Down
down,
/// Left
left,
/// Right
right,
}
extension _PinballDpadDirectionX on PinballDpadDirection {
String toAsset() {
switch (this) {
case PinballDpadDirection.up:
return Assets.images.button.dpadUp.keyName;
case PinballDpadDirection.down:
return Assets.images.button.dpadDown.keyName;
case PinballDpadDirection.left:
return Assets.images.button.dpadLeft.keyName;
case PinballDpadDirection.right:
return Assets.images.button.dpadRight.keyName;
}
}
}
/// {@template pinball_dpad_button}
/// Widget that renders a Dpad button with a given direction.
/// {@endtemplate}
class PinballDpadButton extends StatelessWidget {
/// {@macro pinball_dpad_button}
const PinballDpadButton({
Key? key,
required this.direction,
required this.onTap,
}) : super(key: key);
/// Which [PinballDpadDirection] this button is.
final PinballDpadDirection direction;
/// The function executed when the button is pressed.
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
color: PinballColors.transparent,
child: InkWell(
onTap: onTap,
child: Image.asset(
direction.toAsset(),
width: 60,
height: 60,
),
),
);
}
}

@ -1,4 +1,5 @@
export 'animated_ellipsis_text.dart';
export 'crt_background.dart';
export 'pinball_button.dart';
export 'pinball_dpad_button.dart';
export 'pinball_loading_indicator.dart';

@ -0,0 +1,122 @@
// ignore_for_file: one_member_abstracts
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_ui/gen/gen.dart';
import 'package:pinball_ui/pinball_ui.dart';
extension _WidgetTesterX on WidgetTester {
Future<void> pumpButton({
required PinballDpadDirection direction,
required VoidCallback onTap,
}) async {
await pumpWidget(
MaterialApp(
home: Scaffold(
body: PinballDpadButton(
direction: direction,
onTap: onTap,
),
),
),
);
}
}
extension _CommonFindersX on CommonFinders {
Finder byImagePath(String path) {
return find.byWidgetPredicate(
(widget) {
if (widget is Image) {
final image = widget.image;
if (image is AssetImage) {
return image.keyName == path;
}
return false;
}
return false;
},
);
}
}
abstract class _VoidCallbackStubBase {
void onCall();
}
class _VoidCallbackStub extends Mock implements _VoidCallbackStubBase {}
void main() {
group('PinballDpadButton', () {
testWidgets('can be tapped', (tester) async {
final stub = _VoidCallbackStub();
await tester.pumpButton(
direction: PinballDpadDirection.up,
onTap: stub.onCall,
);
await tester.tap(find.byType(Image));
verify(stub.onCall).called(1);
});
group('up', () {
testWidgets('renders the correct image', (tester) async {
await tester.pumpButton(
direction: PinballDpadDirection.up,
onTap: () {},
);
expect(
find.byImagePath(Assets.images.button.dpadUp.keyName),
findsOneWidget,
);
});
});
group('down', () {
testWidgets('renders the correct image', (tester) async {
await tester.pumpButton(
direction: PinballDpadDirection.down,
onTap: () {},
);
expect(
find.byImagePath(Assets.images.button.dpadDown.keyName),
findsOneWidget,
);
});
});
group('left', () {
testWidgets('renders the correct image', (tester) async {
await tester.pumpButton(
direction: PinballDpadDirection.left,
onTap: () {},
);
expect(
find.byImagePath(Assets.images.button.dpadLeft.keyName),
findsOneWidget,
);
});
});
group('right', () {
testWidgets('renders the correct image', (tester) async {
await tester.pumpButton(
direction: PinballDpadDirection.right,
onTap: () {},
);
expect(
find.byImagePath(Assets.images.button.dpadRight.keyName),
findsOneWidget,
);
});
});
});
}

@ -65,6 +65,7 @@ flutter:
- assets/images/bonus_animation/
- assets/images/score/
- assets/images/link_box/
- assets/images/loading_game/
flutter_gen:
line_length: 80

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

Loading…
Cancel
Save