Merge branch 'feat/animation-sequences' of https://github.com/VGVentures/pinball into feat/animation-sequences

pull/452/head
Allison Ryan 3 years ago
commit d75bad5201

@ -19,21 +19,31 @@ class AssetsManagerCubit extends Cubit<AssetsManagerState> {
/// do its job without adding too much delay for the user, we are letting /// 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. /// the UI paint first, and then we start loading the assets.
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
final loadables = <Future<void> Function()>[
_game.preFetchLeaderboard,
..._game.preLoadAssets(),
..._audioPlayer.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
];
emit( emit(
state.copyWith( state.copyWith(
loadables: [ assetsCount: loadables.length,
_game.preFetchLeaderboard(),
..._game.preLoadAssets(),
..._audioPlayer.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
],
), ),
); );
final all = state.loadables.map((loadable) async {
await loadable; late void Function() _triggerLoad;
emit(state.copyWith(loaded: [...state.loaded, loadable])); _triggerLoad = () async {
}).toList(); if (loadables.isEmpty) return;
await Future.wait(all); final loadable = loadables.removeAt(0);
await loadable();
_triggerLoad();
emit(state.copyWith(loaded: state.loaded + 1));
};
const _throttleSize = 3;
for (var i = 0; i < _throttleSize; i++) {
_triggerLoad();
}
} }
} }

@ -1,44 +1,42 @@
part of 'assets_manager_cubit.dart'; part of 'assets_manager_cubit.dart';
/// {@template assets_manager_state} /// {@template assets_manager_state}
/// State used to load the game assets /// State used to load the game assets.
/// {@endtemplate} /// {@endtemplate}
class AssetsManagerState extends Equatable { class AssetsManagerState extends Equatable {
/// {@macro assets_manager_state} /// {@macro assets_manager_state}
const AssetsManagerState({ const AssetsManagerState({
required this.loadables, required this.assetsCount,
required this.loaded, required this.loaded,
}); });
/// {@macro assets_manager_state} /// {@macro assets_manager_state}
const AssetsManagerState.initial() const AssetsManagerState.initial() : this(assetsCount: 0, loaded: 0);
: this(loadables: const [], loaded: const []);
/// List of futures to load /// Number of assets to load.
final List<Future> loadables; final int assetsCount;
/// List of loaded futures /// Number of already loaded assets.
final List<Future> loaded; final int loaded;
/// Returns a value between 0 and 1 to indicate the loading progress /// Returns a value between 0 and 1 to indicate the loading progress.
double get progress => double get progress => loaded == 0 ? 0 : loaded / assetsCount;
loadables.isEmpty ? 0 : loaded.length / loadables.length;
/// Only returns false if all the assets have been loaded /// Only returns false if all the assets have been loaded.
bool get isLoading => progress != 1; bool get isLoading => progress != 1;
/// Returns a copy of this instance with the given parameters /// Returns a copy of this instance with the given parameters
/// updated /// updated.
AssetsManagerState copyWith({ AssetsManagerState copyWith({
List<Future>? loadables, int? assetsCount,
List<Future>? loaded, int? loaded,
}) { }) {
return AssetsManagerState( return AssetsManagerState(
loadables: loadables ?? this.loadables, assetsCount: assetsCount ?? this.assetsCount,
loaded: loaded ?? this.loaded, loaded: loaded ?? this.loaded,
); );
} }
@override @override
List<Object> get props => [loaded, loadables]; List<Object> get props => [loaded, assetsCount];
} }

@ -13,7 +13,8 @@ class BallSpawningBehavior extends Component
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
if (!newState.status.isPlaying) return false; if (!newState.status.isPlaying) return false;
final startedGame = previousState?.status.isWaiting ?? true; final startedGame = (previousState?.status.isWaiting ?? true) ||
(previousState?.status.isGameOver ?? true);
final lostRound = final lostRound =
(previousState?.rounds ?? newState.rounds + 1) > newState.rounds; (previousState?.rounds ?? newState.rounds + 1) > newState.rounds;
return startedGame || lostRound; return startedGame || lostRound;

@ -7,4 +7,5 @@ export 'camera_focusing_behavior.dart';
export 'character_selection_behavior.dart'; export 'character_selection_behavior.dart';
export 'cow_bumper_noise_behavior.dart'; export 'cow_bumper_noise_behavior.dart';
export 'kicker_noise_behavior.dart'; export 'kicker_noise_behavior.dart';
export 'rollover_noise_behavior.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
@ -7,9 +9,9 @@ import 'package:pinball_components/pinball_components.dart';
/// {@template focus_data} /// {@template focus_data}
/// Defines a [Camera] focus point. /// Defines a [Camera] focus point.
/// {@endtemplate} /// {@endtemplate}
class FocusData { class _FocusData {
/// {@template focus_data} /// {@macro focus_data}
FocusData({ const _FocusData({
required this.zoom, required this.zoom,
required this.position, required this.position,
}); });
@ -24,7 +26,11 @@ class FocusData {
/// Changes the game focus when the [GameBloc] status changes. /// Changes the game focus when the [GameBloc] status changes.
class CameraFocusingBehavior extends Component class CameraFocusingBehavior extends Component
with FlameBlocListenable<GameBloc, GameState>, HasGameRef { with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
late final Map<String, FocusData> _foci; final Map<GameStatus, _FocusData> _foci = {};
GameStatus? _activeFocus;
final _previousSize = Vector2.zero();
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
@ -32,51 +38,62 @@ class CameraFocusingBehavior extends Component
} }
@override @override
void onNewState(GameState state) { void onNewState(GameState state) => _zoomTo(state.status);
switch (state.status) {
case GameStatus.waiting:
break;
case GameStatus.playing:
_zoom(_foci['game']!);
break;
case GameStatus.gameOver:
_zoom(_foci['backbox']!);
break;
}
}
@override @override
Future<void> onLoad() async { void onGameResize(Vector2 size) {
await super.onLoad(); super.onGameResize(size);
_foci = { if (size == _previousSize) {
'game': FocusData( return;
zoom: gameRef.size.y / 16, }
position: Vector2(0, -7.8), _previousSize.setFrom(size);
),
'waiting': FocusData( final maxWidth = size.x / 90;
zoom: gameRef.size.y / 18, final maxHeight = size.y / 160;
final scale = min(maxHeight, maxWidth);
_foci.addAll({
GameStatus.waiting: _FocusData(
zoom: scale + (maxWidth > maxHeight ? 0.3 : -0.5),
position: Vector2(0, -112), position: Vector2(0, -112),
), ),
'backbox': FocusData( GameStatus.playing: _FocusData(
zoom: gameRef.size.y / 10, zoom: scale + (maxWidth > maxHeight ? 0.5 : -0.2),
position: Vector2(0, -7.8),
),
GameStatus.gameOver: _FocusData(
zoom: scale + (maxWidth > maxHeight ? 2.8 : 3.3),
position: Vector2(0, -111), position: Vector2(0, -111),
), ),
}; });
if (_activeFocus != null) {
_snap(_activeFocus!);
}
}
_snap(_foci['waiting']!); @override
Future<void> onLoad() async {
await super.onLoad();
_snap(GameStatus.waiting);
} }
void _snap(FocusData data) { void _snap(GameStatus focusKey) {
final focusData = _foci[_activeFocus = focusKey]!;
gameRef.camera gameRef.camera
..speed = 100 ..speed = 100
..followVector2(data.position) ..followVector2(focusData.position)
..zoom = data.zoom; ..zoom = focusData.zoom;
} }
void _zoom(FocusData data) { void _zoomTo(GameStatus focusKey) {
final zoom = CameraZoom(value: data.zoom); final focusData = _foci[_activeFocus = focusKey]!;
final zoom = CameraZoom(value: focusData.zoom);
zoom.completed.then((_) { zoom.completed.then((_) {
gameRef.camera.moveTo(data.position); gameRef.camera.moveTo(focusData.position);
}); });
add(zoom); add(zoom);
} }

@ -2,8 +2,6 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:platform_helper/platform_helper.dart';
/// Updates the [ArcadeBackground] and launch [Ball] to reflect character /// Updates the [ArcadeBackground] and launch [Ball] to reflect character
/// selections. /// selections.
@ -13,14 +11,12 @@ class CharacterSelectionBehavior extends Component
HasGameRef { HasGameRef {
@override @override
void onNewState(CharacterThemeState state) { void onNewState(CharacterThemeState state) {
if (!readProvider<PlatformHelper>().isMobile) { gameRef
gameRef .descendants()
.descendants() .whereType<ArcadeBackground>()
.whereType<ArcadeBackground>() .single
.single .bloc
.bloc .onCharacterSelected(state.characterTheme);
.onCharacterSelected(state.characterTheme);
}
gameRef gameRef
.descendants() .descendants()
.whereType<Ball>() .whereType<Ball>()

@ -0,0 +1,13 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
class RolloverNoiseBehavior extends ContactBehavior {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
readProvider<PinballAudioPlayer>().play(PinballAudio.rollover);
}
}

@ -19,7 +19,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
static const _maxScore = 9999999999; static const _maxScore = 9999999999;
void _onGameStarted(GameStarted _, Emitter emit) { void _onGameStarted(GameStarted _, Emitter emit) {
emit(state.copyWith(status: GameStatus.playing)); emit(const GameState.initial().copyWith(status: GameStatus.playing));
} }
void _onGameOver(GameOver _, Emitter emit) { void _onGameOver(GameOver _, Emitter emit) {

@ -82,6 +82,9 @@ class GameState extends Equatable {
/// The score displayed at the game. /// The score displayed at the game.
int get displayScore => roundScore + totalScore; int get displayScore => roundScore + totalScore;
/// The max multiplier in game.
bool get isMaxMultiplier => multiplier == 6;
GameState copyWith({ GameState copyWith({
int? totalScore, int? totalScore,
int? roundScore, int? roundScore,

@ -16,41 +16,44 @@ class AndroidAcres extends Component {
AndroidAcres() AndroidAcres()
: super( : super(
children: [ children: [
SpaceshipRamp(
children: [
RampShotBehavior(points: Points.fiveThousand),
RampBonusBehavior(points: Points.oneMillion),
RampProgressBehavior(),
RampMultiplierBehavior(),
RampResetBehavior(),
],
),
SpaceshipRail(),
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(),
CowBumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>( FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>(
create: AndroidSpaceshipCubit.new, create: AndroidSpaceshipCubit.new,
children: [ children: [
SpaceshipRamp(
children: [
RampShotBehavior(points: Points.fiveThousand),
RampBonusBehavior(points: Points.oneMillion),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic( AndroidAnimatronic(
children: [ children: [
ScoringContactBehavior(points: Points.twoHundredThousand), ScoringContactBehavior(points: Points.twoHundredThousand),
], ],
)..initialPosition = Vector2(-26, -28.25), )..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(),
CowBumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(), AndroidSpaceshipBonusBehavior(),
], ],
), ),

@ -1,3 +1,6 @@
export 'android_spaceship_bonus_behavior.dart'; export 'android_spaceship_bonus_behavior.dart';
export 'ramp_bonus_behavior.dart'; export 'ramp_bonus_behavior.dart';
export 'ramp_multiplier_behavior.dart';
export 'ramp_progress_behavior.dart';
export 'ramp_reset_behavior.dart';
export 'ramp_shot_behavior.dart'; export 'ramp_shot_behavior.dart';

@ -1,60 +1,40 @@
import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_bonus_behavior} /// {@template ramp_bonus_behavior}
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp]. /// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class RampBonusBehavior extends Component with ParentIsA<SpaceshipRamp> { class RampBonusBehavior extends Component
with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
/// {@macro ramp_bonus_behavior} /// {@macro ramp_bonus_behavior}
RampBonusBehavior({ RampBonusBehavior({
required Points points, required Points points,
}) : _points = points, }) : _points = points,
super(); super();
/// Creates a [RampBonusBehavior].
///
/// This can be used for testing [RampBonusBehavior] in isolation.
@visibleForTesting
RampBonusBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points; final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override @override
void onMount() { bool listenWhen(
super.onMount(); SpaceshipRampState previousState,
SpaceshipRampState newState,
subscription = subscription ?? ) {
parent.bloc.stream.listen((state) { final hitsIncreased = previousState.hits < newState.hits;
final achievedOneMillionPoints = state.hits % 10 == 0; final achievedOneMillionPoints = newState.hits % 10 == 0;
if (achievedOneMillionPoints) { return hitsIncreased && achievedOneMillionPoints;
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -60),
duration: 2,
),
);
}
});
} }
@override @override
void onRemove() { void onNewState(SpaceshipRampState state) {
subscription?.cancel(); parent!.add(
super.onRemove(); ScoringBehavior(
points: _points,
position: Vector2(0, -60),
duration: 2,
),
);
} }
} }

@ -0,0 +1,27 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Increases the multiplier when a [Ball] is shot 5 times into the
/// [SpaceshipRamp].
class RampMultiplierBehavior extends Component
with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
@override
bool listenWhen(
SpaceshipRampState previousState,
SpaceshipRampState newState,
) {
final hitsIncreased = previousState.hits < newState.hits;
final achievedFiveShots = newState.hits % 5 == 0;
final notMaxMultiplier =
!readBloc<GameBloc, GameState>().state.isMaxMultiplier;
return hitsIncreased & achievedFiveShots && notMaxMultiplier;
}
@override
void onNewState(SpaceshipRampState state) {
readBloc<GameBloc, GameState>().add(const MultiplierIncreased());
}
}

@ -0,0 +1,34 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Changes arrow lit when a [Ball] is shot into the [SpaceshipRamp].
class RampProgressBehavior extends Component
with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
@override
bool listenWhen(
SpaceshipRampState previousState,
SpaceshipRampState newState,
) {
return previousState.hits < newState.hits;
}
@override
void onNewState(SpaceshipRampState state) {
final gameBloc = readBloc<GameBloc, GameState>();
final spaceshipCubit = readBloc<SpaceshipRampCubit, SpaceshipRampState>();
final canProgress = !gameBloc.state.isMaxMultiplier ||
(gameBloc.state.isMaxMultiplier && !state.arrowFullyLit);
if (canProgress) {
spaceshipCubit.onProgressed();
}
if (spaceshipCubit.state.arrowFullyLit && !gameBloc.state.isMaxMultiplier) {
spaceshipCubit.onProgressed();
}
}
}

@ -0,0 +1,19 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Reset [SpaceshipRamp] state when GameState.rounds changes.
class RampResetBehavior extends Component
with FlameBlocListenable<GameBloc, GameState> {
@override
bool listenWhen(GameState previousState, GameState newState) {
return previousState.rounds != newState.rounds;
}
@override
void onNewState(GameState state) {
readBloc<SpaceshipRampCubit, SpaceshipRampState>().onReset();
}
}

@ -1,64 +1,36 @@
import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_shot_behavior} /// {@template ramp_shot_behavior}
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. /// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class RampShotBehavior extends Component class RampShotBehavior extends Component
with ParentIsA<SpaceshipRamp>, FlameBlocReader<GameBloc, GameState> { with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
/// {@macro ramp_shot_behavior} /// {@macro ramp_shot_behavior}
RampShotBehavior({ RampShotBehavior({
required Points points, required Points points,
}) : _points = points, }) : _points = points,
super(); super();
/// Creates a [RampShotBehavior].
///
/// This can be used for testing [RampShotBehavior] in isolation.
@visibleForTesting
RampShotBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points; final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override @override
void onMount() { bool listenWhen(
super.onMount(); SpaceshipRampState previousState,
SpaceshipRampState newState,
subscription = subscription ?? ) {
parent.bloc.stream.listen((state) { return previousState.hits < newState.hits;
final achievedOneMillionPoints = state.hits % 10 == 0;
if (!achievedOneMillionPoints) {
bloc.add(const MultiplierIncreased());
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -45),
),
);
}
});
} }
@override @override
void onRemove() { void onNewState(SpaceshipRampState state) {
subscription?.cancel(); parent!.add(
super.onRemove(); ScoringBehavior(
points: _points,
position: Vector2(0, -45),
),
);
} }
} }

@ -66,7 +66,7 @@ class GameOverInfoDisplay extends Component with HasGameRef {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
gameRef.overlays.add(PinballGame.playButtonOverlay); gameRef.overlays.add(PinballGame.replayButtonOverlay);
} }
} }
@ -290,7 +290,7 @@ class OpenSourceTextComponent extends TextComponent with HasGameRef, Tappable {
); );
@override @override
bool onTapDown(TapDownInfo info) { bool onTapUp(TapUpInfo info) {
openLink(ShareRepository.openSourceCode); openLink(ShareRepository.openSourceCode);
return true; return true;
} }

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

@ -1,76 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template controlled_plunger}
/// A [Plunger] with a [PlungerController] attached.
/// {@endtemplate}
class ControlledPlunger extends Plunger with Controls<PlungerController> {
/// {@macro controlled_plunger}
ControlledPlunger({required double compressionDistance})
: super(compressionDistance: compressionDistance) {
controller = PlungerController(this);
}
@override
void release() {
super.release();
add(PlungerNoiseBehavior());
}
}
/// A behavior attached to the plunger when it launches the ball which plays the
/// related sound effects.
class PlungerNoiseBehavior extends Component {
@override
Future<void> onLoad() async {
await super.onLoad();
readProvider<PinballAudioPlayer>().play(PinballAudio.launcher);
}
@override
void update(double dt) {
super.update(dt);
removeFromParent();
}
}
/// {@template plunger_controller}
/// A [ComponentController] that controls a [Plunger]s movement.
/// {@endtemplate}
class PlungerController extends ComponentController<Plunger>
with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro plunger_controller}
PlungerController(Plunger plunger) : super(plunger);
/// The [LogicalKeyboardKey]s that will control the [Flipper].
///
/// [onKeyEvent] method listens to when one of these keys is pressed.
static const List<LogicalKeyboardKey> _keys = [
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyS,
];
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (bloc.state.status.isGameOver) return true;
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
component.pull();
} else if (event is RawKeyUpEvent) {
component.release();
}
return false;
}
}

@ -5,6 +5,7 @@ import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:platform_helper/platform_helper.dart';
/// Listens to the [GameBloc] and updates the game accordingly. /// Listens to the [GameBloc] and updates the game accordingly.
class GameBlocStatusListener extends Component class GameBlocStatusListener extends Component
@ -21,12 +22,17 @@ class GameBlocStatusListener extends Component
break; break;
case GameStatus.playing: case GameStatus.playing:
readProvider<PinballAudioPlayer>().play(PinballAudio.backgroundMusic); readProvider<PinballAudioPlayer>().play(PinballAudio.backgroundMusic);
_resetBonuses();
gameRef gameRef
.descendants() .descendants()
.whereType<Flipper>() .whereType<Flipper>()
.forEach(_addFlipperKeyControls); .forEach(_addFlipperBehaviors);
gameRef
.descendants()
.whereType<Plunger>()
.forEach(_addPlungerBehaviors);
gameRef.overlays.remove(PinballGame.playButtonOverlay); gameRef.overlays.remove(PinballGame.playButtonOverlay);
gameRef.overlays.remove(PinballGame.replayButtonOverlay);
break; break;
case GameStatus.gameOver: case GameStatus.gameOver:
readProvider<PinballAudioPlayer>().play(PinballAudio.gameOverVoiceOver); readProvider<PinballAudioPlayer>().play(PinballAudio.gameOverVoiceOver);
@ -36,22 +42,63 @@ class GameBlocStatusListener extends Component
.state .state
.characterTheme, .characterTheme,
); );
gameRef gameRef
.descendants() .descendants()
.whereType<Flipper>() .whereType<Flipper>()
.forEach(_removeFlipperKeyControls); .forEach(_removeFlipperBehaviors);
gameRef
.descendants()
.whereType<Plunger>()
.forEach(_removePlungerBehaviors);
break; break;
} }
} }
void _addFlipperKeyControls(Flipper flipper) { void _resetBonuses() {
flipper gameRef
..add(FlipperKeyControllingBehavior()) .descendants()
..moveDown(); .whereType<FlameBlocProvider<GoogleWordCubit, GoogleWordState>>()
.single
.bloc
.onReset();
} }
void _removeFlipperKeyControls(Flipper flipper) => flipper void _addPlungerBehaviors(Plunger plunger) {
final platformHelper = readProvider<PlatformHelper>();
const pullingStrength = 7.0;
final provider =
plunger.firstChild<FlameBlocProvider<PlungerCubit, PlungerState>>()!;
if (platformHelper.isMobile) {
provider.add(
PlungerAutoPullingBehavior(strength: pullingStrength),
);
} else {
provider.addAll(
[
PlungerKeyControllingBehavior(),
PlungerPullingBehavior(strength: pullingStrength),
],
);
}
}
void _removePlungerBehaviors(Plunger plunger) {
plunger
.descendants()
.whereType<PlungerPullingBehavior>()
.forEach(plunger.remove);
plunger
.descendants()
.whereType<PlungerKeyControllingBehavior>()
.forEach(plunger.remove);
}
void _addFlipperBehaviors(Flipper flipper) => flipper
.firstChild<FlameBlocProvider<FlipperCubit, FlipperState>>()!
.add(FlipperKeyControllingBehavior());
void _removeFlipperBehaviors(Flipper flipper) => flipper
.descendants() .descendants()
.whereType<FlipperKeyControllingBehavior>() .whereType<FlipperKeyControllingBehavior>()
.forEach(flipper.remove); .forEach(flipper.remove);

@ -17,8 +17,9 @@ class GoogleWordBonusBehavior extends Component {
onNewState: (state) { onNewState: (state) {
readBloc<GameBloc, GameState>() readBloc<GameBloc, GameState>()
.add(const BonusActivated(GameBonus.googleWord)); .add(const BonusActivated(GameBonus.googleWord));
readBloc<GoogleWordCubit, GoogleWordState>().onBonusAwarded(); readBloc<GoogleWordCubit, GoogleWordState>().onReset();
add(BonusBallSpawningBehavior()); add(BonusBallSpawningBehavior());
add(GoogleWordAnimatingBehavior());
}, },
), ),
); );

@ -22,12 +22,14 @@ class GoogleGallery extends Component with ZIndex {
side: BoardSide.right, side: BoardSide.right,
children: [ children: [
ScoringContactBehavior(points: Points.fiveThousand), ScoringContactBehavior(points: Points.fiveThousand),
RolloverNoiseBehavior(),
], ],
), ),
GoogleRollover( GoogleRollover(
side: BoardSide.left, side: BoardSide.left,
children: [ children: [
ScoringContactBehavior(points: Points.fiveThousand), ScoringContactBehavior(points: Points.fiveThousand),
RolloverNoiseBehavior(),
], ],
), ),
GoogleWord(position: Vector2(-4.45, 1.8)), GoogleWord(position: Vector2(-4.45, 1.8)),

@ -1,5 +1,4 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:pinball/game/components/components.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template launcher} /// {@template launcher}
@ -13,8 +12,7 @@ class Launcher extends Component {
children: [ children: [
LaunchRamp(), LaunchRamp(),
Flapper(), Flapper(),
ControlledPlunger(compressionDistance: 9.2) Plunger()..initialPosition = Vector2(41, 43.7),
..initialPosition = Vector2(41, 43.7),
RocketSpriteComponent()..position = Vector2(42.8, 62.3), RocketSpriteComponent()..position = Vector2(42.8, 62.3),
], ],
); );

@ -6,163 +6,196 @@ import 'package:pinball_theme/pinball_theme.dart' hide Assets;
/// Add methods to help loading and caching game assets. /// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame { extension PinballGameAssetsX on PinballGame {
/// Returns a list of assets to be loaded /// Returns a list of assets to be loaded
List<Future<Image>> preLoadAssets() { List<Future<Image> Function()> preLoadAssets() {
const dashTheme = DashTheme(); const dashTheme = DashTheme();
const sparkyTheme = SparkyTheme(); const sparkyTheme = SparkyTheme();
const androidTheme = AndroidTheme(); const androidTheme = AndroidTheme();
const dinoTheme = DinoTheme(); const dinoTheme = DinoTheme();
return [
final gameAssets = [ () => images.load(components.Assets.images.boardBackground.keyName),
images.load(components.Assets.images.boardBackground.keyName), () => images.load(components.Assets.images.ball.flameEffect.keyName),
images.load(components.Assets.images.ball.flameEffect.keyName), () => images.load(components.Assets.images.signpost.inactive.keyName),
images.load(components.Assets.images.signpost.inactive.keyName), () => images.load(components.Assets.images.signpost.active1.keyName),
images.load(components.Assets.images.signpost.active1.keyName), () => images.load(components.Assets.images.signpost.active2.keyName),
images.load(components.Assets.images.signpost.active2.keyName), () => images.load(components.Assets.images.signpost.active3.keyName),
images.load(components.Assets.images.signpost.active3.keyName), () => images.load(components.Assets.images.flipper.left.keyName),
images.load(components.Assets.images.flipper.left.keyName), () => images.load(components.Assets.images.flipper.right.keyName),
images.load(components.Assets.images.flipper.right.keyName), () => images.load(components.Assets.images.baseboard.left.keyName),
images.load(components.Assets.images.baseboard.left.keyName), () => images.load(components.Assets.images.baseboard.right.keyName),
images.load(components.Assets.images.baseboard.right.keyName), () => images.load(components.Assets.images.kicker.left.lit.keyName),
images.load(components.Assets.images.kicker.left.lit.keyName), () => images.load(components.Assets.images.kicker.left.dimmed.keyName),
images.load(components.Assets.images.kicker.left.dimmed.keyName), () => images.load(components.Assets.images.kicker.right.lit.keyName),
images.load(components.Assets.images.kicker.right.lit.keyName), () => images.load(components.Assets.images.kicker.right.dimmed.keyName),
images.load(components.Assets.images.kicker.right.dimmed.keyName), () => images.load(components.Assets.images.slingshot.upper.keyName),
images.load(components.Assets.images.slingshot.upper.keyName), () => images.load(components.Assets.images.slingshot.lower.keyName),
images.load(components.Assets.images.slingshot.lower.keyName), () => images.load(components.Assets.images.launchRamp.ramp.keyName),
images.load(components.Assets.images.launchRamp.ramp.keyName), () => images.load(
images.load( components.Assets.images.launchRamp.foregroundRailing.keyName,
components.Assets.images.launchRamp.foregroundRailing.keyName, ),
), () => images.load(
images.load( components.Assets.images.launchRamp.backgroundRailing.keyName,
components.Assets.images.launchRamp.backgroundRailing.keyName, ),
), () => images.load(components.Assets.images.dino.bottomWall.keyName),
images.load(components.Assets.images.dino.bottomWall.keyName), () => images.load(components.Assets.images.dino.topWall.keyName),
images.load(components.Assets.images.dino.topWall.keyName), () => images.load(components.Assets.images.dino.topWallTunnel.keyName),
images.load(components.Assets.images.dino.topWallTunnel.keyName), () => images.load(components.Assets.images.dino.animatronic.head.keyName),
images.load(components.Assets.images.dino.animatronic.head.keyName), () =>
images.load(components.Assets.images.dino.animatronic.mouth.keyName), images.load(components.Assets.images.dino.animatronic.mouth.keyName),
images.load(components.Assets.images.dash.animatronic.keyName), () => images.load(components.Assets.images.dash.animatronic.keyName),
images.load(components.Assets.images.dash.bumper.a.active.keyName), () => images.load(components.Assets.images.dash.bumper.a.active.keyName),
images.load(components.Assets.images.dash.bumper.a.inactive.keyName), () =>
images.load(components.Assets.images.dash.bumper.b.active.keyName), images.load(components.Assets.images.dash.bumper.a.inactive.keyName),
images.load(components.Assets.images.dash.bumper.b.inactive.keyName), () => images.load(components.Assets.images.dash.bumper.b.active.keyName),
images.load(components.Assets.images.dash.bumper.main.active.keyName), () =>
images.load(components.Assets.images.dash.bumper.main.inactive.keyName), images.load(components.Assets.images.dash.bumper.b.inactive.keyName),
images.load(components.Assets.images.plunger.plunger.keyName), () =>
images.load(components.Assets.images.plunger.rocket.keyName), images.load(components.Assets.images.dash.bumper.main.active.keyName),
images.load(components.Assets.images.boundary.bottom.keyName), () => images
images.load(components.Assets.images.boundary.outer.keyName), .load(components.Assets.images.dash.bumper.main.inactive.keyName),
images.load(components.Assets.images.boundary.outerBottom.keyName), () => images.load(components.Assets.images.plunger.plunger.keyName),
images.load(components.Assets.images.android.spaceship.saucer.keyName), () => images.load(components.Assets.images.plunger.rocket.keyName),
images () => images.load(components.Assets.images.boundary.bottom.keyName),
() => images.load(components.Assets.images.boundary.outer.keyName),
() => images.load(components.Assets.images.boundary.outerBottom.keyName),
() => images
.load(components.Assets.images.android.spaceship.saucer.keyName),
() => images
.load(components.Assets.images.android.spaceship.animatronic.keyName), .load(components.Assets.images.android.spaceship.animatronic.keyName),
images.load(components.Assets.images.android.spaceship.lightBeam.keyName), () => images
images.load(components.Assets.images.android.ramp.boardOpening.keyName), .load(components.Assets.images.android.spaceship.lightBeam.keyName),
images.load( () => images
components.Assets.images.android.ramp.railingForeground.keyName, .load(components.Assets.images.android.ramp.boardOpening.keyName),
), () => images.load(
images.load( components.Assets.images.android.ramp.railingForeground.keyName,
components.Assets.images.android.ramp.railingBackground.keyName, ),
), () => images.load(
images.load(components.Assets.images.android.ramp.main.keyName), components.Assets.images.android.ramp.railingBackground.keyName,
images.load(components.Assets.images.android.ramp.arrow.inactive.keyName), ),
images.load( () => images.load(components.Assets.images.android.ramp.main.keyName),
components.Assets.images.android.ramp.arrow.active1.keyName, () => images
), .load(components.Assets.images.android.ramp.arrow.inactive.keyName),
images.load( () => images.load(
components.Assets.images.android.ramp.arrow.active2.keyName, components.Assets.images.android.ramp.arrow.active1.keyName,
), ),
images.load( () => images.load(
components.Assets.images.android.ramp.arrow.active3.keyName, components.Assets.images.android.ramp.arrow.active2.keyName,
), ),
images.load( () => images.load(
components.Assets.images.android.ramp.arrow.active4.keyName, components.Assets.images.android.ramp.arrow.active3.keyName,
), ),
images.load( () => images.load(
components.Assets.images.android.ramp.arrow.active5.keyName, components.Assets.images.android.ramp.arrow.active4.keyName,
), ),
images.load(components.Assets.images.android.rail.main.keyName), () => images.load(
images.load(components.Assets.images.android.rail.exit.keyName), components.Assets.images.android.ramp.arrow.active5.keyName,
images.load(components.Assets.images.android.bumper.a.lit.keyName), ),
images.load(components.Assets.images.android.bumper.a.dimmed.keyName), () => images.load(components.Assets.images.android.rail.main.keyName),
images.load(components.Assets.images.android.bumper.b.lit.keyName), () => images.load(components.Assets.images.android.rail.exit.keyName),
images.load(components.Assets.images.android.bumper.b.dimmed.keyName), () => images.load(components.Assets.images.android.bumper.a.lit.keyName),
images.load(components.Assets.images.android.bumper.cow.lit.keyName), () =>
images.load(components.Assets.images.android.bumper.cow.dimmed.keyName), images.load(components.Assets.images.android.bumper.a.dimmed.keyName),
images.load(components.Assets.images.sparky.computer.top.keyName), () => images.load(components.Assets.images.android.bumper.b.lit.keyName),
images.load(components.Assets.images.sparky.computer.base.keyName), () =>
images.load(components.Assets.images.sparky.computer.glow.keyName), images.load(components.Assets.images.android.bumper.b.dimmed.keyName),
images.load(components.Assets.images.sparky.animatronic.keyName), () =>
images.load(components.Assets.images.sparky.bumper.a.lit.keyName), images.load(components.Assets.images.android.bumper.cow.lit.keyName),
images.load(components.Assets.images.sparky.bumper.a.dimmed.keyName), () => images
images.load(components.Assets.images.sparky.bumper.b.lit.keyName), .load(components.Assets.images.android.bumper.cow.dimmed.keyName),
images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName), () => images.load(components.Assets.images.sparky.computer.top.keyName),
images.load(components.Assets.images.sparky.bumper.c.lit.keyName), () => images.load(components.Assets.images.sparky.computer.base.keyName),
images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName), () => images.load(components.Assets.images.sparky.computer.glow.keyName),
images.load(components.Assets.images.backbox.marquee.keyName), () => images.load(components.Assets.images.sparky.animatronic.keyName),
images.load(components.Assets.images.backbox.displayDivider.keyName), () => images.load(components.Assets.images.sparky.bumper.a.lit.keyName),
images.load(components.Assets.images.backbox.button.facebook.keyName), () =>
images.load(components.Assets.images.backbox.button.twitter.keyName), images.load(components.Assets.images.sparky.bumper.a.dimmed.keyName),
images.load( () => images.load(components.Assets.images.sparky.bumper.b.lit.keyName),
components.Assets.images.backbox.displayTitleDecoration.keyName, () =>
), images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter1.lit.keyName), () => images.load(components.Assets.images.sparky.bumper.c.lit.keyName),
images.load(components.Assets.images.googleWord.letter1.dimmed.keyName), () =>
images.load(components.Assets.images.googleWord.letter2.lit.keyName), images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter2.dimmed.keyName), () => images.load(components.Assets.images.backbox.marquee.keyName),
images.load(components.Assets.images.googleWord.letter3.lit.keyName), () =>
images.load(components.Assets.images.googleWord.letter3.dimmed.keyName), images.load(components.Assets.images.backbox.displayDivider.keyName),
images.load(components.Assets.images.googleWord.letter4.lit.keyName), () =>
images.load(components.Assets.images.googleWord.letter4.dimmed.keyName), images.load(components.Assets.images.backbox.button.facebook.keyName),
images.load(components.Assets.images.googleWord.letter5.lit.keyName), () =>
images.load(components.Assets.images.googleWord.letter5.dimmed.keyName), images.load(components.Assets.images.backbox.button.twitter.keyName),
images.load(components.Assets.images.googleWord.letter6.lit.keyName), () => images.load(
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName), components.Assets.images.backbox.displayTitleDecoration.keyName,
images.load(components.Assets.images.googleRollover.left.decal.keyName), ),
images.load(components.Assets.images.googleRollover.left.pin.keyName), () =>
images.load(components.Assets.images.googleRollover.right.decal.keyName), images.load(components.Assets.images.googleWord.letter1.lit.keyName),
images.load(components.Assets.images.googleRollover.right.pin.keyName), () => images
images.load(components.Assets.images.multiball.lit.keyName), .load(components.Assets.images.googleWord.letter1.dimmed.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName), () =>
images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.googleWord.letter2.lit.keyName),
images.load(components.Assets.images.multiplier.x2.dimmed.keyName), () => images
images.load(components.Assets.images.multiplier.x3.lit.keyName), .load(components.Assets.images.googleWord.letter2.dimmed.keyName),
images.load(components.Assets.images.multiplier.x3.dimmed.keyName), () =>
images.load(components.Assets.images.multiplier.x4.lit.keyName), images.load(components.Assets.images.googleWord.letter3.lit.keyName),
images.load(components.Assets.images.multiplier.x4.dimmed.keyName), () => images
images.load(components.Assets.images.multiplier.x5.lit.keyName), .load(components.Assets.images.googleWord.letter3.dimmed.keyName),
images.load(components.Assets.images.multiplier.x5.dimmed.keyName), () =>
images.load(components.Assets.images.multiplier.x6.lit.keyName), images.load(components.Assets.images.googleWord.letter4.lit.keyName),
images.load(components.Assets.images.multiplier.x6.dimmed.keyName), () => images
images.load(components.Assets.images.score.fiveThousand.keyName), .load(components.Assets.images.googleWord.letter4.dimmed.keyName),
images.load(components.Assets.images.score.twentyThousand.keyName), () =>
images.load(components.Assets.images.score.twoHundredThousand.keyName), images.load(components.Assets.images.googleWord.letter5.lit.keyName),
images.load(components.Assets.images.score.oneMillion.keyName), () => images
images.load(components.Assets.images.flapper.backSupport.keyName), .load(components.Assets.images.googleWord.letter5.dimmed.keyName),
images.load(components.Assets.images.flapper.frontSupport.keyName), () =>
images.load(components.Assets.images.flapper.flap.keyName), images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.skillShot.decal.keyName), () => images
images.load(components.Assets.images.skillShot.pin.keyName), .load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.skillShot.lit.keyName), () => images
images.load(components.Assets.images.skillShot.dimmed.keyName), .load(components.Assets.images.googleRollover.left.decal.keyName),
images.load(components.Assets.images.displayArrows.arrowLeft.keyName), () =>
images.load(components.Assets.images.displayArrows.arrowRight.keyName), images.load(components.Assets.images.googleRollover.left.pin.keyName),
images.load(androidTheme.leaderboardIcon.keyName), () => images
images.load(androidTheme.ball.keyName), .load(components.Assets.images.googleRollover.right.decal.keyName),
images.load(dashTheme.leaderboardIcon.keyName), () => images
images.load(dashTheme.ball.keyName), .load(components.Assets.images.googleRollover.right.pin.keyName),
images.load(dinoTheme.leaderboardIcon.keyName), () => images.load(components.Assets.images.multiball.lit.keyName),
images.load(dinoTheme.ball.keyName), () => images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName), () => images.load(components.Assets.images.multiplier.x2.lit.keyName),
images.load(sparkyTheme.ball.keyName), () => images.load(components.Assets.images.multiplier.x2.dimmed.keyName),
() => images.load(components.Assets.images.multiplier.x3.lit.keyName),
() => images.load(components.Assets.images.multiplier.x3.dimmed.keyName),
() => images.load(components.Assets.images.multiplier.x4.lit.keyName),
() => images.load(components.Assets.images.multiplier.x4.dimmed.keyName),
() => images.load(components.Assets.images.multiplier.x5.lit.keyName),
() => images.load(components.Assets.images.multiplier.x5.dimmed.keyName),
() => images.load(components.Assets.images.multiplier.x6.lit.keyName),
() => images.load(components.Assets.images.multiplier.x6.dimmed.keyName),
() => images.load(components.Assets.images.score.fiveThousand.keyName),
() => images.load(components.Assets.images.score.twentyThousand.keyName),
() => images
.load(components.Assets.images.score.twoHundredThousand.keyName),
() => images.load(components.Assets.images.score.oneMillion.keyName),
() => images.load(components.Assets.images.flapper.backSupport.keyName),
() => images.load(components.Assets.images.flapper.frontSupport.keyName),
() => images.load(components.Assets.images.flapper.flap.keyName),
() => images.load(components.Assets.images.skillShot.decal.keyName),
() => images.load(components.Assets.images.skillShot.pin.keyName),
() => images.load(components.Assets.images.skillShot.lit.keyName),
() => images.load(components.Assets.images.skillShot.dimmed.keyName),
() =>
images.load(components.Assets.images.displayArrows.arrowLeft.keyName),
() => images
.load(components.Assets.images.displayArrows.arrowRight.keyName),
() => images.load(androidTheme.leaderboardIcon.keyName),
() => images.load(androidTheme.ball.keyName),
() => images.load(dashTheme.leaderboardIcon.keyName),
() => images.load(dashTheme.ball.keyName),
() => images.load(dinoTheme.leaderboardIcon.keyName),
() => images.load(dinoTheme.ball.keyName),
() => images.load(sparkyTheme.leaderboardIcon.keyName),
() => images.load(sparkyTheme.ball.keyName),
() => images.load(androidTheme.background.keyName),
() => images.load(dashTheme.background.keyName),
() => images.load(dinoTheme.background.keyName),
() => images.load(sparkyTheme.background.keyName),
]; ];
return (platformHelper.isMobile) ? gameAssets : gameAssets
..addAll([
images.load(androidTheme.background.keyName),
images.load(dashTheme.background.keyName),
images.load(dinoTheme.background.keyName),
images.load(sparkyTheme.background.keyName),
]);
} }
} }

@ -38,10 +38,13 @@ class PinballGame extends PinballForge2DGame
images.prefix = ''; images.prefix = '';
} }
/// Identifier of the play button overlay /// Identifier of the play button overlay.
static const playButtonOverlay = 'play_button'; static const playButtonOverlay = 'play_button';
/// Identifier of the mobile controls overlay /// Identifier of the replay button overlay.
static const replayButtonOverlay = 'replay_button';
/// Identifier of the mobile controls overlay.
static const mobileControlsOverlay = 'mobile_controls'; static const mobileControlsOverlay = 'mobile_controls';
@override @override
@ -125,6 +128,7 @@ class PinballGame extends PinballForge2DGame
SkillShot( SkillShot(
children: [ children: [
ScoringContactBehavior(points: Points.oneMillion), ScoringContactBehavior(points: Points.oneMillion),
RolloverNoiseBehavior(),
], ],
), ),
AndroidAcres(), AndroidAcres(),
@ -155,17 +159,28 @@ class PinballGame extends PinballForge2DGame
final rocket = descendants().whereType<RocketSpriteComponent>().first; final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size; final bounds = rocket.topLeftPosition & rocket.size;
// NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. // NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586
if (bounds.contains(info.eventPosition.game.toOffset())) { // we need to check it at the highest level manually.
descendants().whereType<Plunger>().single.pullFor(2); final tappedRocket = bounds.contains(info.eventPosition.game.toOffset());
if (tappedRocket) {
descendants()
.whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
.first
.bloc
.pulled();
} else { } else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; final tappedLeftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide[pointerId] = focusedBoardSide[pointerId] =
leftSide ? BoardSide.left : BoardSide.right; tappedLeftSide ? BoardSide.left : BoardSide.right;
final flippers = descendants().whereType<Flipper>().where((flipper) { final flippers = descendants()
return flipper.side == focusedBoardSide[pointerId]; .whereType<Flipper>()
}); .where((flipper) => flipper.side == focusedBoardSide[pointerId]);
flippers.first.moveUp(); for (final flipper in flippers) {
flipper
.descendants()
.whereType<FlameBlocProvider<FlipperCubit, FlipperState>>()
.forEach((provider) => provider.bloc.moveUp());
}
} }
} }
@ -186,11 +201,15 @@ class PinballGame extends PinballForge2DGame
void _moveFlippersDown(int pointerId) { void _moveFlippersDown(int pointerId) {
if (focusedBoardSide[pointerId] != null) { if (focusedBoardSide[pointerId] != null) {
final flippers = descendants().whereType<Flipper>().where((flipper) { final flippers = descendants()
return flipper.side == focusedBoardSide[pointerId]; .whereType<Flipper>()
}); .where((flipper) => flipper.side == focusedBoardSide[pointerId]);
flippers.first.moveDown(); for (final flipper in flippers) {
focusedBoardSide.remove(pointerId); flipper
.descendants()
.whereType<FlameBlocProvider<FlipperCubit, FlipperState>>()
.forEach((provider) => provider.bloc.moveDown());
}
} }
} }
} }
@ -220,9 +239,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await add(PreviewLine()); await addAll([PreviewLine(), _DebugInformation()]);
await add(_DebugInformation());
} }
@override @override
@ -237,14 +254,10 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
} }
@override @override
void onPanStart(DragStartInfo info) { void onPanStart(DragStartInfo info) => lineStart = info.eventPosition.game;
lineStart = info.eventPosition.game;
}
@override @override
void onPanUpdate(DragUpdateInfo info) { void onPanUpdate(DragUpdateInfo info) => lineEnd = info.eventPosition.game;
lineEnd = info.eventPosition.game;
}
@override @override
void onPanEnd(DragEndInfo info) { void onPanEnd(DragEndInfo info) {

@ -100,22 +100,25 @@ class PinballGameLoadedView extends StatelessWidget {
focusNode: game.focusNode, focusNode: game.focusNode,
initialActiveOverlays: const [PinballGame.playButtonOverlay], initialActiveOverlays: const [PinballGame.playButtonOverlay],
overlayBuilderMap: { overlayBuilderMap: {
PinballGame.playButtonOverlay: (context, game) { PinballGame.playButtonOverlay: (_, game) => const Positioned(
return const Positioned( bottom: 20,
bottom: 20, right: 0,
right: 0, left: 0,
left: 0, child: PlayButtonOverlay(),
child: PlayButtonOverlay(), ),
); PinballGame.mobileControlsOverlay: (_, game) => Positioned(
}, bottom: 0,
PinballGame.mobileControlsOverlay: (context, game) { left: 0,
return Positioned( right: 0,
bottom: 0, child: MobileControls(game: game),
left: 0, ),
right: 0, PinballGame.replayButtonOverlay: (context, game) =>
child: MobileControls(game: game), const Positioned(
); bottom: 20,
}, right: 0,
left: 0,
child: ReplayButtonOverlay(),
)
}, },
), ),
), ),

@ -72,14 +72,16 @@ class BonusAnimation extends StatefulWidget {
final VoidCallback? _onCompleted; final VoidCallback? _onCompleted;
/// Returns a list of assets to be loaded for animations. /// Returns a list of assets to be loaded for animations.
static List<Future> loadAssets() { static List<Future Function()> loadAssets() {
Flame.images.prefix = ''; Flame.images.prefix = '';
return [ return [
Flame.images.load(Assets.images.bonusAnimation.dashNest.keyName), () => Flame.images.load(Assets.images.bonusAnimation.dashNest.keyName),
Flame.images.load(Assets.images.bonusAnimation.sparkyTurboCharge.keyName), () => Flame.images
Flame.images.load(Assets.images.bonusAnimation.dinoChomp.keyName), .load(Assets.images.bonusAnimation.sparkyTurboCharge.keyName),
Flame.images.load(Assets.images.bonusAnimation.androidSpaceship.keyName), () => Flame.images.load(Assets.images.bonusAnimation.dinoChomp.keyName),
Flame.images.load(Assets.images.bonusAnimation.googleWord.keyName), () => Flame.images
.load(Assets.images.bonusAnimation.androidSpaceship.keyName),
() => Flame.images.load(Assets.images.bonusAnimation.googleWord.keyName),
]; ];
} }

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -18,6 +19,7 @@ class ReplayButtonOverlay extends StatelessWidget {
return PinballButton( return PinballButton(
text: l10n.replay, text: l10n.replay,
onTap: () { onTap: () {
context.read<GameBloc>().add(const GameStarted());
context.read<StartGameBloc>().add(const ReplayTapped()); context.read<StartGameBloc>().add(const ReplayTapped());
}, },
); );

@ -213,7 +213,7 @@
"@socialMediaAccount": { "@socialMediaAccount": {
"description": "Text displayed on share screen for description" "description": "Text displayed on share screen for description"
}, },
"iGotScoreAtPinball": "I got {score} at the #IOPinball machine, can you beat my score? See you at #GoogleIO!", "iGotScoreAtPinball": "I got {score} points in #IOPinball, can you beat my score? \nSee you at #GoogleIO!",
"@iGotScoreAtPinball": { "@iGotScoreAtPinball": {
"description": "Text to share score on Social Network", "description": "Text to share score on Social Network",
"placeholders": { "placeholders": {

@ -1,5 +1,3 @@
import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart'; import 'package:authentication_repository/authentication_repository.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
@ -17,11 +15,8 @@ void main() {
final authenticationRepository = AuthenticationRepository(firebaseAuth); final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudioPlayer = PinballAudioPlayer(); final pinballAudioPlayer = PinballAudioPlayer();
final platformHelper = PlatformHelper(); final platformHelper = PlatformHelper();
unawaited( await Firebase.initializeApp();
Firebase.initializeApp().then( await authenticationRepository.authenticateAnonymously();
(_) => authenticationRepository.authenticateAnonymously(),
),
);
return App( return App(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,

@ -132,10 +132,15 @@ class _Character extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(
child: Opacity( child: Opacity(
opacity: isSelected ? 1 : 0.3, opacity: isSelected ? 1 : 0.4,
child: TextButton( child: TextButton(
onPressed: () => onPressed: () =>
context.read<CharacterThemeCubit>().characterSelected(character), context.read<CharacterThemeCubit>().characterSelected(character),
style: ButtonStyle(
overlayColor: MaterialStateProperty.all(
PinballColors.transparent,
),
),
child: character.icon.image(fit: BoxFit.contain), child: character.icon.image(fit: BoxFit.contain),
), ),
), ),

@ -22,12 +22,12 @@ class SelectedCharacter extends StatefulWidget {
State<SelectedCharacter> createState() => _SelectedCharacterState(); State<SelectedCharacter> createState() => _SelectedCharacterState();
/// Returns a list of assets to be loaded. /// Returns a list of assets to be loaded.
static List<Future> loadAssets() { static List<Future Function()> loadAssets() {
return [ return [
Flame.images.load(const DashTheme().animation.keyName), () => Flame.images.load(const DashTheme().animation.keyName),
Flame.images.load(const AndroidTheme().animation.keyName), () => Flame.images.load(const AndroidTheme().animation.keyName),
Flame.images.load(const DinoTheme().animation.keyName), () => Flame.images.load(const DinoTheme().animation.keyName),
Flame.images.load(const SparkyTheme().animation.keyName), () => Flame.images.load(const SparkyTheme().animation.keyName),
]; ];
} }
} }

@ -26,6 +26,7 @@ class $AssetsSfxGen {
String get kickerA => 'assets/sfx/kicker_a.mp3'; String get kickerA => 'assets/sfx/kicker_a.mp3';
String get kickerB => 'assets/sfx/kicker_b.mp3'; String get kickerB => 'assets/sfx/kicker_b.mp3';
String get launcher => 'assets/sfx/launcher.mp3'; String get launcher => 'assets/sfx/launcher.mp3';
String get rollover => 'assets/sfx/rollover.mp3';
String get sparky => 'assets/sfx/sparky.mp3'; String get sparky => 'assets/sfx/sparky.mp3';
} }

@ -33,6 +33,9 @@ enum PinballAudio {
/// Kicker. /// Kicker.
kicker, kicker,
/// Rollover.
rollover,
/// Sparky. /// Sparky.
sparky, sparky,
@ -56,7 +59,7 @@ typedef CreateAudioPool = Future<AudioPool> Function(
}); });
/// Defines the contract for playing a single audio. /// Defines the contract for playing a single audio.
typedef PlaySingleAudio = Future<void> Function(String); typedef PlaySingleAudio = Future<void> Function(String, {double volume});
/// Defines the contract for looping a single audio. /// Defines the contract for looping a single audio.
typedef LoopSingleAudio = Future<void> Function(String, {double volume}); typedef LoopSingleAudio = Future<void> Function(String, {double volume});
@ -81,18 +84,20 @@ class _SimplePlayAudio extends _Audio {
required this.preCacheSingleAudio, required this.preCacheSingleAudio,
required this.playSingleAudio, required this.playSingleAudio,
required this.path, required this.path,
this.volume,
}); });
final PreCacheSingleAudio preCacheSingleAudio; final PreCacheSingleAudio preCacheSingleAudio;
final PlaySingleAudio playSingleAudio; final PlaySingleAudio playSingleAudio;
final String path; final String path;
final double? volume;
@override @override
Future<void> load() => preCacheSingleAudio(prefixFile(path)); Future<void> load() => preCacheSingleAudio(prefixFile(path));
@override @override
void play() { void play() {
playSingleAudio(prefixFile(path)); playSingleAudio(prefixFile(path), volume: volume ?? 1);
} }
} }
@ -266,6 +271,12 @@ class PinballAudioPlayer {
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.launcher, path: Assets.sfx.launcher,
), ),
PinballAudio.rollover: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.rollover,
volume: 0.3,
),
PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( PinballAudio.ioPinballVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
@ -323,10 +334,10 @@ class PinballAudioPlayer {
late final Map<PinballAudio, _Audio> audios; late final Map<PinballAudio, _Audio> audios;
/// Loads the sounds effects into the memory. /// Loads the sounds effects into the memory.
List<Future<void>> load() { List<Future<void> Function()> load() {
_configureAudioCache(FlameAudio.audioCache); _configureAudioCache(FlameAudio.audioCache);
return audios.values.map((a) => a.load()).toList(); return audios.values.map((a) => a.load).toList();
} }
/// Plays the received audio. /// Plays the received audio.

@ -29,15 +29,15 @@ class _MockConfigureAudioCache extends Mock {
} }
class _MockPlaySingleAudio extends Mock { class _MockPlaySingleAudio extends Mock {
Future<void> onCall(String url); Future<void> onCall(String path, {double volume});
} }
class _MockLoopSingleAudio extends Mock { class _MockLoopSingleAudio extends Mock {
Future<void> onCall(String url, {double volume}); Future<void> onCall(String path, {double volume});
} }
abstract class _PreCacheSingleAudio { abstract class _PreCacheSingleAudio {
Future<void> onCall(String url); Future<void> onCall(String path);
} }
class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {} class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {}
@ -74,7 +74,8 @@ void main() {
when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); when(() => configureAudioCache.onCall(any())).thenAnswer((_) {});
playSingleAudio = _MockPlaySingleAudio(); playSingleAudio = _MockPlaySingleAudio();
when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {}); when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume')))
.thenAnswer((_) async {});
loopSingleAudio = _MockLoopSingleAudio(); loopSingleAudio = _MockLoopSingleAudio();
when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume'))) when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume')))
@ -101,7 +102,9 @@ void main() {
group('load', () { group('load', () {
test('creates the bumpers pools', () async { test('creates the bumpers pools', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
verify( verify(
() => createAudioPool.onCall( () => createAudioPool.onCall(
@ -121,7 +124,9 @@ void main() {
}); });
test('creates the kicker pools', () async { test('creates the kicker pools', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
verify( verify(
() => createAudioPool.onCall( () => createAudioPool.onCall(
@ -141,7 +146,9 @@ void main() {
}); });
test('configures the audio cache instance', () async { test('configures the audio cache instance', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
verify(() => configureAudioCache.onCall(FlameAudio.audioCache)) verify(() => configureAudioCache.onCall(FlameAudio.audioCache))
.called(1); .called(1);
@ -153,13 +160,17 @@ void main() {
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall,
); );
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
expect(FlameAudio.audioCache.prefix, equals('')); expect(FlameAudio.audioCache.prefix, equals(''));
}); });
test('pre cache the assets', () async { test('pre cache the assets', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
@ -195,6 +206,10 @@ void main() {
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/launcher.mp3'), .onCall('packages/pinball_audio/assets/sfx/launcher.mp3'),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/rollover.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/cow_moo.mp3'), .onCall('packages/pinball_audio/assets/sfx/cow_moo.mp3'),
@ -237,7 +252,9 @@ void main() {
group('when seed is true', () { group('when seed is true', () {
test('plays the bumper A sound pool', () async { test('plays the bumper A sound pool', () async {
when(seed.nextBool).thenReturn(true); when(seed.nextBool).thenReturn(true);
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.bumper); audioPlayer.play(PinballAudio.bumper);
verify(() => bumperAPool.start(volume: 0.6)).called(1); verify(() => bumperAPool.start(volume: 0.6)).called(1);
@ -247,7 +264,9 @@ void main() {
group('when seed is false', () { group('when seed is false', () {
test('plays the bumper B sound pool', () async { test('plays the bumper B sound pool', () async {
when(seed.nextBool).thenReturn(false); when(seed.nextBool).thenReturn(false);
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.bumper); audioPlayer.play(PinballAudio.bumper);
verify(() => bumperBPool.start(volume: 0.6)).called(1); verify(() => bumperBPool.start(volume: 0.6)).called(1);
@ -286,7 +305,9 @@ void main() {
group('when seed is true', () { group('when seed is true', () {
test('plays the kicker A sound pool', () async { test('plays the kicker A sound pool', () async {
when(seed.nextBool).thenReturn(true); when(seed.nextBool).thenReturn(true);
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.kicker); audioPlayer.play(PinballAudio.kicker);
verify(() => kickerAPool.start(volume: 0.6)).called(1); verify(() => kickerAPool.start(volume: 0.6)).called(1);
@ -296,7 +317,9 @@ void main() {
group('when seed is false', () { group('when seed is false', () {
test('plays the kicker B sound pool', () async { test('plays the kicker B sound pool', () async {
when(seed.nextBool).thenReturn(false); when(seed.nextBool).thenReturn(false);
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.kicker); audioPlayer.play(PinballAudio.kicker);
verify(() => kickerBPool.start(volume: 0.6)).called(1); verify(() => kickerBPool.start(volume: 0.6)).called(1);
@ -306,7 +329,9 @@ void main() {
group('cow moo', () { group('cow moo', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.cowMoo); audioPlayer.play(PinballAudio.cowMoo);
verify( verify(
@ -319,7 +344,9 @@ void main() {
final clock = _MockClock(); final clock = _MockClock();
await withClock(clock, () async { await withClock(clock, () async {
when(clock.now).thenReturn(DateTime(2022)); when(clock.now).thenReturn(DateTime(2022));
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer audioPlayer
..play(PinballAudio.cowMoo) ..play(PinballAudio.cowMoo)
..play(PinballAudio.cowMoo); ..play(PinballAudio.cowMoo);
@ -342,84 +369,127 @@ void main() {
group('google', () { group('google', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.google); audioPlayer.play(PinballAudio.google);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.google}'), 'packages/pinball_audio/${Assets.sfx.google}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
group('sparky', () { group('sparky', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.sparky); audioPlayer.play(PinballAudio.sparky);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.sparky}'), 'packages/pinball_audio/${Assets.sfx.sparky}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
group('dino', () { group('dino', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.dino); audioPlayer.play(PinballAudio.dino);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.dino}'), 'packages/pinball_audio/${Assets.sfx.dino}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
group('android', () { group('android', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.android); audioPlayer.play(PinballAudio.android);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.android}'), 'packages/pinball_audio/${Assets.sfx.android}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
group('dash', () { group('dash', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.dash); audioPlayer.play(PinballAudio.dash);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.dash}'), 'packages/pinball_audio/${Assets.sfx.dash}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
group('launcher', () { group('launcher', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.launcher); audioPlayer.play(PinballAudio.launcher);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.launcher}'), 'packages/pinball_audio/${Assets.sfx.launcher}',
volume: any(named: 'volume'),
),
).called(1);
});
});
group('rollover', () {
test('plays the correct file', () async {
await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.rollover);
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.rollover}',
volume: .3,
),
).called(1); ).called(1);
}); });
}); });
group('ioPinballVoiceOver', () { group('ioPinballVoiceOver', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.ioPinballVoiceOver); audioPlayer.play(PinballAudio.ioPinballVoiceOver);
verify( verify(
() => playSingleAudio.onCall( () => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.ioPinballVoiceOver}', 'packages/pinball_audio/${Assets.sfx.ioPinballVoiceOver}',
volume: any(named: 'volume'),
), ),
).called(1); ).called(1);
}); });
@ -427,12 +497,15 @@ void main() {
group('gameOverVoiceOver', () { group('gameOverVoiceOver', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.gameOverVoiceOver); audioPlayer.play(PinballAudio.gameOverVoiceOver);
verify( verify(
() => playSingleAudio.onCall( () => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.gameOverVoiceOver}', 'packages/pinball_audio/${Assets.sfx.gameOverVoiceOver}',
volume: any(named: 'volume'),
), ),
).called(1); ).called(1);
}); });
@ -440,7 +513,9 @@ void main() {
group('backgroundMusic', () { group('backgroundMusic', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.backgroundMusic); audioPlayer.play(PinballAudio.backgroundMusic);
verify( verify(
@ -452,7 +527,9 @@ void main() {
}); });
test('plays only once', () async { test('plays only once', () async {
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer audioPlayer
..play(PinballAudio.backgroundMusic) ..play(PinballAudio.backgroundMusic)
..play(PinballAudio.backgroundMusic); ..play(PinballAudio.backgroundMusic);
@ -470,7 +547,9 @@ void main() {
'throws assertions error when playing an unregistered audio', 'throws assertions error when playing an unregistered audio',
() async { () async {
audioPlayer.audios.remove(PinballAudio.google); audioPlayer.audios.remove(PinballAudio.google);
await Future.wait(audioPlayer.load()); await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
expect( expect(
() => audioPlayer.play(PinballAudio.google), () => audioPlayer.play(PinballAudio.google),

@ -1,6 +1,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/android_animatronic/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template android_animatronic} /// {@template android_animatronic}
@ -13,6 +15,7 @@ class AndroidAnimatronic extends BodyComponent
: super( : super(
children: [ children: [
_AndroidAnimatronicSpriteAnimationComponent(), _AndroidAnimatronicSpriteAnimationComponent(),
AndroidAnimatronicBallContactBehavior(),
...?children, ...?children,
], ],
renderBody: false, renderBody: false,
@ -21,6 +24,13 @@ class AndroidAnimatronic extends BodyComponent
zIndex = ZIndexes.androidHead; zIndex = ZIndexes.androidHead;
} }
/// Creates an [AndroidAnimatronic] without any children.
///
/// This can be used for testing [AndroidAnimatronic]'s behaviors in
/// isolation.
@visibleForTesting
AndroidAnimatronic.test();
@override @override
Body createBody() { Body createBody() {
final shape = EllipseShape( final shape = EllipseShape(

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

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

@ -5,7 +5,6 @@ import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_spaceship_cubit.dart'; export 'cubit/android_spaceship_cubit.dart';
@ -17,9 +16,6 @@ class AndroidSpaceship extends Component {
_SpaceshipSaucer()..initialPosition = position, _SpaceshipSaucer()..initialPosition = position,
_SpaceshipSaucerSpriteAnimationComponent()..position = position, _SpaceshipSaucerSpriteAnimationComponent()..position = position,
_LightBeamSpriteComponent()..position = position + Vector2(2.5, 5), _LightBeamSpriteComponent()..position = position + Vector2(2.5, 5),
AndroidSpaceshipEntrance(
children: [AndroidSpaceshipEntranceBallContactBehavior()],
),
_SpaceshipHole( _SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail, outsideLayer: Layer.spaceshipExitRail,
outsidePriority: ZIndexes.ballOnSpaceshipRail, outsidePriority: ZIndexes.ballOnSpaceshipRail,
@ -134,35 +130,6 @@ class _LightBeamSpriteComponent extends SpriteComponent
} }
} }
class AndroidSpaceshipEntrance extends BodyComponent
with ParentIsA<AndroidSpaceship>, Layered {
AndroidSpaceshipEntrance({Iterable<Component>? children})
: super(
children: children,
renderBody: false,
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
2,
0.1,
Vector2(-27.4, -37.2),
-0.12,
);
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef();
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _SpaceshipHole extends LayerSensor { class _SpaceshipHole extends LayerSensor {
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority}) _SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
: super( : super(

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

@ -5,7 +5,7 @@ part 'android_spaceship_state.dart';
class AndroidSpaceshipCubit extends Cubit<AndroidSpaceshipState> { class AndroidSpaceshipCubit extends Cubit<AndroidSpaceshipState> {
AndroidSpaceshipCubit() : super(AndroidSpaceshipState.withoutBonus); AndroidSpaceshipCubit() : super(AndroidSpaceshipState.withoutBonus);
void onBallEntered() => emit(AndroidSpaceshipState.withBonus); void onBallContacted() => emit(AndroidSpaceshipState.withBonus);
void onBonusAwarded() => emit(AndroidSpaceshipState.withoutBonus); void onBonusAwarded() => emit(AndroidSpaceshipState.withoutBonus);
} }

@ -57,7 +57,7 @@ class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final spriteSheet = await gameRef.images.load( final spriteSheet = gameRef.images.fromCache(
Assets.images.ball.flameEffect.keyName, Assets.images.ball.flameEffect.keyName,
); );

@ -109,7 +109,7 @@ class _OuterBoundary extends BodyComponent with InitialPosition, ZIndex {
final topLeftCurve = BezierCurveShape( final topLeftCurve = BezierCurveShape(
controlPoints: [ controlPoints: [
topWall.vertex1, topWall.vertex2,
Vector2(-31.5, -69.9), Vector2(-31.5, -69.9),
Vector2(-32.3, -57.2), Vector2(-32.3, -57.2),
], ],
@ -123,7 +123,7 @@ class _OuterBoundary extends BodyComponent with InitialPosition, ZIndex {
final upperLeftWallCurve = BezierCurveShape( final upperLeftWallCurve = BezierCurveShape(
controlPoints: [ controlPoints: [
topLeftWall.vertex1, topLeftWall.vertex2,
Vector2(-33.9, -40.7), Vector2(-33.9, -40.7),
Vector2(-32.5, -39), Vector2(-32.5, -39),
], ],

@ -7,7 +7,9 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@endtemplate} /// {@endtemplate}
class BumpingBehavior extends ContactBehavior { class BumpingBehavior extends ContactBehavior {
/// {@macro bumping_behavior} /// {@macro bumping_behavior}
BumpingBehavior({required double strength}) : _strength = strength; BumpingBehavior({required double strength})
: assert(strength >= 0, "Strength can't be negative."),
_strength = strength;
/// Determines how strong the bump is. /// Determines how strong the bump is.
final double _strength; final double _strength;

@ -1,4 +1,4 @@
export 'android_animatronic.dart'; export 'android_animatronic/android_animatronic.dart';
export 'android_bumper/android_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'android_spaceship/android_spaceship.dart'; export 'android_spaceship/android_spaceship.dart';
export 'arcade_background/arcade_background.dart'; export 'arcade_background/arcade_background.dart';
@ -27,7 +27,7 @@ export 'launch_ramp.dart';
export 'layer_sensor/layer_sensor.dart'; export 'layer_sensor/layer_sensor.dart';
export 'multiball/multiball.dart'; export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart'; export 'multiplier/multiplier.dart';
export 'plunger.dart'; export 'plunger/plunger.dart';
export 'rocket.dart'; export 'rocket.dart';
export 'score_component/score_component.dart'; export 'score_component/score_component.dart';
export 'signpost/signpost.dart'; export 'signpost/signpost.dart';

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

@ -1,11 +1,11 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Allows controlling the [Flipper]'s movement with keyboard input. /// Allows controlling the [Flipper]'s movement with keyboard input.
class FlipperKeyControllingBehavior extends Component class FlipperKeyControllingBehavior extends Component
with KeyboardHandler, ParentIsA<Flipper> { with KeyboardHandler, FlameBlocReader<FlipperCubit, FlipperState> {
/// The [LogicalKeyboardKey]s that will control the [Flipper]. /// The [LogicalKeyboardKey]s that will control the [Flipper].
/// ///
/// [onKeyEvent] method listens to when one of these keys is pressed. /// [onKeyEvent] method listens to when one of these keys is pressed.
@ -14,7 +14,21 @@ class FlipperKeyControllingBehavior extends Component
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
_keys = parent.side.flipperKeys; final flipper = parent!.parent! as Flipper;
switch (flipper.side) {
case BoardSide.left:
_keys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
];
break;
case BoardSide.right:
_keys = [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
];
break;
}
} }
@override @override
@ -25,28 +39,11 @@ class FlipperKeyControllingBehavior extends Component
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {
parent.moveUp(); bloc.moveUp();
} else if (event is RawKeyUpEvent) { } else if (event is RawKeyUpEvent) {
parent.moveDown(); bloc.moveDown();
} }
return false; return false;
} }
} }
extension on BoardSide {
List<LogicalKeyboardKey> get flipperKeys {
switch (this) {
case BoardSide.left:
return [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
];
case BoardSide.right:
return [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
];
}
}
}

@ -0,0 +1,40 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_components/pinball_components.dart';
class FlipperMovingBehavior extends Component
with
FlameBlocListenable<FlipperCubit, FlipperState>,
FlameBlocReader<FlipperCubit, FlipperState> {
FlipperMovingBehavior({
required double strength,
}) : assert(strength >= 0, "Strength can't be negative"),
_strength = strength;
final double _strength;
late final Flipper _flipper;
void _moveUp() => _flipper.body.linearVelocity = Vector2(0, -_strength);
void _moveDown() => _flipper.body.linearVelocity = Vector2(0, _strength);
@override
void onNewState(FlipperState state) {
super.onNewState(state);
if (bloc.state.isMovingDown) _moveDown();
}
@override
void update(double dt) {
super.update(dt);
if (bloc.state.isMovingUp) _moveUp();
}
@override
Future<void> onLoad() async {
await super.onLoad();
_flipper = parent!.parent! as Flipper;
_moveDown();
}
}

@ -0,0 +1,11 @@
import 'package:bloc/bloc.dart';
part 'flipper_state.dart';
class FlipperCubit extends Cubit<FlipperState> {
FlipperCubit() : super(FlipperState.movingDown);
void moveUp() => emit(FlipperState.movingUp);
void moveDown() => emit(FlipperState.movingDown);
}

@ -0,0 +1,11 @@
part of 'flipper_cubit.dart';
enum FlipperState {
movingDown,
movingUp,
}
extension FlipperStateX on FlipperState {
bool get isMovingDown => this == FlipperState.movingDown;
bool get isMovingUp => this == FlipperState.movingUp;
}

@ -1,11 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
export 'behaviors/behaviors.dart'; export 'behaviors/behaviors.dart';
export 'cubit/flipper_cubit.dart';
/// {@template flipper} /// {@template flipper}
/// A bat, typically found in pairs at the bottom of the board. /// A bat, typically found in pairs at the bottom of the board.
@ -21,6 +23,10 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
children: [ children: [
_FlipperSpriteComponent(side: side), _FlipperSpriteComponent(side: side),
FlipperJointingBehavior(), FlipperJointingBehavior(),
FlameBlocProvider<FlipperCubit, FlipperState>(
create: FlipperCubit.new,
children: [FlipperMovingBehavior(strength: 90)],
),
], ],
); );
@ -33,29 +39,12 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
/// The size of the [Flipper]. /// The size of the [Flipper].
static final size = Vector2(13.5, 4.3); static final size = Vector2(13.5, 4.3);
/// The speed required to move the [Flipper] to its highest position.
///
/// The higher the value, the faster the [Flipper] will move.
static const double _speed = 90;
/// Whether the [Flipper] is on the left or right side of the board. /// Whether the [Flipper] is on the left or right side of the board.
/// ///
/// A [Flipper] with [BoardSide.left] has a counter-clockwise arc motion, /// A [Flipper] with [BoardSide.left] has a counter-clockwise arc motion,
/// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion. /// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion.
final BoardSide side; final BoardSide side;
/// Applies downward linear velocity to the [Flipper], moving it to its
/// resting position.
void moveDown() {
body.linearVelocity = Vector2(0, _speed);
}
/// Applies upward linear velocity to the [Flipper], moving it to its highest
/// position.
void moveUp() {
body.linearVelocity = Vector2(0, -_speed);
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final direction = side.direction; final direction = side.direction;

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_components/pinball_components.dart';
class GoogleWordAnimatingBehavior extends TimerComponent
with FlameBlocReader<GoogleWordCubit, GoogleWordState> {
GoogleWordAnimatingBehavior() : super(period: 0.35, repeat: true);
final _maxBlinks = 7;
int _blinks = 0;
@override
void onTick() {
super.onTick();
if (_blinks != _maxBlinks * 2) {
bloc.switched();
_blinks++;
} else {
timer.stop();
bloc.onReset();
shouldRemove = true;
}
}
}

@ -23,7 +23,52 @@ class GoogleWordCubit extends Cubit<GoogleWordState> {
} }
} }
void switched() {
switch (state.letterSpriteStates[0]!) {
case GoogleLetterSpriteState.lit:
emit(
GoogleWordState(
letterSpriteStates: {
for (int i = 0; i < _lettersInGoogle; i++)
if (i.isEven)
i: GoogleLetterSpriteState.dimmed
else
i: GoogleLetterSpriteState.lit
},
),
);
break;
case GoogleLetterSpriteState.dimmed:
emit(
GoogleWordState(
letterSpriteStates: {
for (int i = 0; i < _lettersInGoogle; i++)
if (i.isEven)
i: GoogleLetterSpriteState.lit
else
i: GoogleLetterSpriteState.dimmed
},
),
);
break;
}
}
void onBonusAwarded() { void onBonusAwarded() {
emit(
GoogleWordState(
letterSpriteStates: {
for (int i = 0; i < _lettersInGoogle; i++)
if (i.isEven)
i: GoogleLetterSpriteState.lit
else
i: GoogleLetterSpriteState.dimmed
},
),
);
}
void onReset() {
emit(GoogleWordState.initial()); emit(GoogleWordState.initial());
_lastLitLetter = 0; _lastLitLetter = 0;
} }

@ -1,6 +1,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
export 'behaviors/behaviors.dart';
export 'cubit/google_word_cubit.dart'; export 'cubit/google_word_cubit.dart';
/// {@template google_word} /// {@template google_word}

@ -1,251 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the
/// play field.
///
/// [Plunger] ignores gravity so the player controls its downward [pull].
/// {@endtemplate}
class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger}
Plunger({
required this.compressionDistance,
}) : super(
renderBody: false,
children: [_PlungerSpriteAnimationGroupComponent()],
) {
zIndex = ZIndexes.plunger;
layer = Layer.launcher;
}
/// Creates a [Plunger] without any children.
///
/// This can be used for testing [Plunger]'s behaviors in isolation.
@visibleForTesting
Plunger.test({required this.compressionDistance});
/// Distance the plunger can lower.
final double compressionDistance;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final leftShapeVertices = [
Vector2(0, 0),
Vector2(-1.8, 0),
Vector2(-1.8, -2.2),
Vector2(0, -0.3),
]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle))
.toList();
final leftTriangleShape = PolygonShape()..set(leftShapeVertices);
final leftTriangleFixtureDef = FixtureDef(leftTriangleShape)..density = 80;
fixturesDef.add(leftTriangleFixtureDef);
final rightShapeVertices = [
Vector2(0, 0),
Vector2(1.8, 0),
Vector2(1.8, -2.2),
Vector2(0, -0.3),
]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle))
.toList();
final rightTriangleShape = PolygonShape()..set(rightShapeVertices);
final rightTriangleFixtureDef = FixtureDef(rightTriangleShape)
..density = 80;
fixturesDef.add(rightTriangleFixtureDef);
return fixturesDef;
}
@override
Body createBody() {
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
type: BodyType.dynamic,
gravityScale: Vector2.zero(),
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
var _pullingDownTime = 0.0;
/// Pulls the plunger down for the given amount of [seconds].
// ignore: use_setters_to_change_properties
void pullFor(double seconds) {
_pullingDownTime = seconds;
}
/// Set a constant downward velocity on the [Plunger].
void pull() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
body.linearVelocity = Vector2(0, 7);
sprite.pull();
}
/// Set an upward velocity on the [Plunger].
///
/// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition].
void release() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
_pullingDownTime = 0;
final velocity = (initialPosition.y - body.position.y) * 11;
body.linearVelocity = Vector2(0, velocity);
sprite.release();
}
@override
void update(double dt) {
// Ensure that we only pull or release when the time is greater than zero.
if (_pullingDownTime > 0) {
_pullingDownTime -= PinballForge2DGame.clampDt(dt);
if (_pullingDownTime <= 0) {
release();
} else {
pull();
}
}
super.update(dt);
}
/// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical
/// motion.
Future<void> _anchorToJoint() async {
final anchor = PlungerAnchor(plunger: this);
await add(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: this,
anchor: anchor,
);
world.createJoint(
PrismaticJoint(jointDef)..setLimits(-compressionDistance, 0),
);
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
}
}
/// Animation states associated with a [Plunger].
enum _PlungerAnimationState {
/// Pull state.
pull,
/// Release state.
release,
}
/// Animations for pulling and releasing [Plunger].
class _PlungerSpriteAnimationGroupComponent
extends SpriteAnimationGroupComponent<_PlungerAnimationState>
with HasGameRef {
_PlungerSpriteAnimationGroupComponent()
: super(
anchor: Anchor.center,
position: Vector2(1.87, 14.9),
);
void pull() {
if (current != _PlungerAnimationState.pull) {
animation?.reset();
}
current = _PlungerAnimationState.pull;
}
void release() {
if (current != _PlungerAnimationState.release) {
animation?.reset();
}
current = _PlungerAnimationState.release;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.plunger.plunger.keyName,
);
const amountPerRow = 20;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
final pullAnimation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn ~/ 2,
amountPerRow: amountPerRow ~/ 2,
stepTime: 1 / 24,
textureSize: textureSize,
texturePosition: Vector2.zero(),
loop: false,
),
);
animations = {
_PlungerAnimationState.release: pullAnimation.reversed(),
_PlungerAnimationState.pull: pullAnimation,
};
current = _PlungerAnimationState.release;
}
}
/// {@template plunger_anchor}
/// [JointAnchor] positioned below a [Plunger].
/// {@endtemplate}
class PlungerAnchor extends JointAnchor {
/// {@macro plunger_anchor}
PlungerAnchor({
required Plunger plunger,
}) {
initialPosition = Vector2(
0,
plunger.compressionDistance,
);
}
}
/// {@template plunger_anchor_prismatic_joint_def}
/// [PrismaticJointDef] between a [Plunger] and an [JointAnchor] with motion on
/// the vertical axis.
///
/// The [Plunger] is constrained vertically between its starting position and
/// the [JointAnchor]. The [JointAnchor] must be below the [Plunger].
/// {@endtemplate}
class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
/// {@macro plunger_anchor_prismatic_joint_def}
PlungerAnchorPrismaticJointDef({
required Plunger plunger,
required PlungerAnchor anchor,
}) {
initialize(
plunger.body,
anchor.body,
plunger.body.position + anchor.body.position,
Vector2(16, BoardDimensions.bounds.height),
);
enableLimit = true;
lowerTranslation = double.negativeInfinity;
enableMotor = true;
motorSpeed = 1000;
maxMotorForce = motorSpeed;
collideConnected = true;
}
}

@ -0,0 +1,5 @@
export 'plunger_jointing_behavior.dart';
export 'plunger_key_controlling_behavior.dart';
export 'plunger_noise_behavior.dart';
export 'plunger_pulling_behavior.dart';
export 'plunger_releasing_behavior.dart';

@ -0,0 +1,54 @@
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';
class PlungerJointingBehavior extends Component with ParentIsA<Plunger> {
PlungerJointingBehavior({required double compressionDistance})
: _compressionDistance = compressionDistance;
final double _compressionDistance;
@override
Future<void> onLoad() async {
await super.onLoad();
final anchor = JointAnchor()
..initialPosition = Vector2(0, _compressionDistance);
await add(anchor);
final jointDef = _PlungerAnchorPrismaticJointDef(
plunger: parent,
anchor: anchor,
);
parent.world.createJoint(
PrismaticJoint(jointDef)..setLimits(-_compressionDistance, 0),
);
}
}
/// [PrismaticJointDef] between a [Plunger] and an [JointAnchor] with motion on
/// the vertical axis.
///
/// The [Plunger] is constrained vertically between its starting position and
/// the [JointAnchor]. The [JointAnchor] must be below the [Plunger].
class _PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
/// {@macro plunger_anchor_prismatic_joint_def}
_PlungerAnchorPrismaticJointDef({
required Plunger plunger,
required BodyComponent anchor,
}) {
initialize(
plunger.body,
anchor.body,
plunger.body.position + anchor.body.position,
Vector2(16, BoardDimensions.bounds.height),
);
enableLimit = true;
lowerTranslation = double.negativeInfinity;
enableMotor = true;
motorSpeed = 1000;
maxMotorForce = motorSpeed;
collideConnected = true;
}
}

@ -0,0 +1,33 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
/// Allows controlling the [Plunger]'s movement with keyboard input.
class PlungerKeyControllingBehavior extends Component
with KeyboardHandler, FlameBlocReader<PlungerCubit, PlungerState> {
/// The [LogicalKeyboardKey]s that will control the [Plunger].
///
/// [onKeyEvent] method listens to when one of these keys is pressed.
static const List<LogicalKeyboardKey> _keys = [
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyS,
];
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
bloc.pulled();
} else if (event is RawKeyUpEvent) {
bloc.released();
}
return false;
}
}

@ -0,0 +1,19 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Plays the [PinballAudio.launcher] sound.
///
/// It is attached when the plunger is released.
class PlungerNoiseBehavior extends Component
with FlameBlocListenable<PlungerCubit, PlungerState> {
@override
void onNewState(PlungerState state) {
super.onNewState(state);
if (state.isReleasing) {
readProvider<PinballAudioPlayer>().play(PinballAudio.launcher);
}
}
}

@ -0,0 +1,46 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
class PlungerPullingBehavior extends Component
with FlameBlocReader<PlungerCubit, PlungerState> {
PlungerPullingBehavior({
required double strength,
}) : assert(strength >= 0, "Strength can't be negative."),
_strength = strength;
final double _strength;
late final Plunger _plunger;
@override
Future<void> onLoad() async {
await super.onLoad();
_plunger = parent!.parent! as Plunger;
}
@override
void update(double dt) {
if (bloc.state.isPulling) {
_plunger.body.linearVelocity = Vector2(0, _strength);
}
}
}
class PlungerAutoPullingBehavior extends PlungerPullingBehavior {
PlungerAutoPullingBehavior({
required double strength,
}) : super(strength: strength);
@override
void update(double dt) {
super.update(dt);
final joint = _plunger.body.joints.whereType<PrismaticJoint>().single;
final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit();
if (reachedBottom) {
bloc.released();
}
}
}

@ -0,0 +1,31 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_components/pinball_components.dart';
class PlungerReleasingBehavior extends Component
with FlameBlocListenable<PlungerCubit, PlungerState> {
PlungerReleasingBehavior({
required double strength,
}) : assert(strength >= 0, "Strength can't be negative."),
_strength = strength;
final double _strength;
late final Plunger _plunger;
@override
Future<void> onLoad() async {
await super.onLoad();
_plunger = parent!.parent! as Plunger;
}
@override
void onNewState(PlungerState state) {
super.onNewState(state);
if (state.isReleasing) {
final velocity =
(_plunger.initialPosition.y - _plunger.body.position.y) * _strength;
_plunger.body.linearVelocity = Vector2(0, velocity);
}
}
}

@ -0,0 +1,11 @@
import 'package:bloc/bloc.dart';
part 'plunger_state.dart';
class PlungerCubit extends Cubit<PlungerState> {
PlungerCubit() : super(PlungerState.releasing);
void pulled() => emit(PlungerState.pulling);
void released() => emit(PlungerState.releasing);
}

@ -0,0 +1,12 @@
part of 'plunger_cubit.dart';
enum PlungerState {
pulling,
releasing,
}
extension PlungerStateX on PlungerState {
bool get isPulling => this == PlungerState.pulling;
bool get isReleasing => this == PlungerState.releasing;
}

@ -0,0 +1,139 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'behaviors/behaviors.dart';
export 'cubit/plunger_cubit.dart';
/// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the
/// play field.
///
/// [Plunger] ignores gravity so the player controls its downward movement.
/// {@endtemplate}
class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger}
Plunger()
: super(
renderBody: false,
children: [
FlameBlocProvider<PlungerCubit, PlungerState>(
create: PlungerCubit.new,
children: [
_PlungerSpriteAnimationGroupComponent(),
PlungerReleasingBehavior(strength: 11),
PlungerNoiseBehavior(),
],
),
PlungerJointingBehavior(compressionDistance: 9.2),
],
) {
zIndex = ZIndexes.plunger;
layer = Layer.launcher;
}
/// Creates a [Plunger] without any children.
///
/// This can be used for testing [Plunger]'s behaviors in isolation.
@visibleForTesting
Plunger.test();
List<FixtureDef> _createFixtureDefs() {
final leftShapeVertices = [
Vector2(0, 0),
Vector2(-1.8, 0),
Vector2(-1.8, -2.2),
Vector2(0, -0.3),
]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle));
final leftTriangleShape = PolygonShape()..set(leftShapeVertices);
final rightShapeVertices = [
Vector2(0, 0),
Vector2(1.8, 0),
Vector2(1.8, -2.2),
Vector2(0, -0.3),
]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle));
final rightTriangleShape = PolygonShape()..set(rightShapeVertices);
return [
FixtureDef(
leftTriangleShape,
density: 80,
),
FixtureDef(
rightTriangleShape,
density: 80,
),
];
}
@override
Body createBody() {
final bodyDef = BodyDef(
position: initialPosition,
type: BodyType.dynamic,
gravityScale: Vector2.zero(),
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
}
class _PlungerSpriteAnimationGroupComponent
extends SpriteAnimationGroupComponent<PlungerState>
with HasGameRef, FlameBlocListenable<PlungerCubit, PlungerState> {
_PlungerSpriteAnimationGroupComponent()
: super(
anchor: Anchor.center,
position: Vector2(1.87, 14.9),
);
@override
void onNewState(PlungerState state) {
super.onNewState(state);
final startedReleasing = state.isReleasing && !current!.isReleasing;
final startedPulling = state.isPulling && !current!.isPulling;
if (startedReleasing || startedPulling) {
animation?.reset();
}
current = state;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.plunger.plunger.keyName,
);
const amountPerRow = 20;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
final pullAnimation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn ~/ 2,
amountPerRow: amountPerRow ~/ 2,
stepTime: 1 / 24,
textureSize: textureSize,
texturePosition: Vector2.zero(),
loop: false,
),
);
animations = {
PlungerState.releasing: pullAnimation.reversed(),
PlungerState.pulling: pullAnimation,
};
current = readBloc<PlungerCubit, PlungerState>().state;
}
}

@ -16,7 +16,8 @@ class RampBallAscendingContactBehavior
if (other is! Ball) return; if (other is! Ball) return;
if (other.body.linearVelocity.y < 0) { if (other.body.linearVelocity.y < 0) {
parent.parent.bloc.onAscendingBallEntered(); readBloc<SpaceshipRampCubit, SpaceshipRampState>()
.onAscendingBallEntered();
} }
} }
} }

@ -6,9 +6,17 @@ part 'spaceship_ramp_state.dart';
class SpaceshipRampCubit extends Cubit<SpaceshipRampState> { class SpaceshipRampCubit extends Cubit<SpaceshipRampState> {
SpaceshipRampCubit() : super(const SpaceshipRampState.initial()); SpaceshipRampCubit() : super(const SpaceshipRampState.initial());
void onAscendingBallEntered() { void onAscendingBallEntered() => emit(state.copyWith(hits: state.hits + 1));
void onProgressed() {
final index = ArrowLightState.values.indexOf(state.lightState);
emit( emit(
state.copyWith(hits: state.hits + 1), state.copyWith(
lightState:
ArrowLightState.values[(index + 1) % ArrowLightState.values.length],
),
); );
} }
void onReset() => emit(const SpaceshipRampState.initial());
} }

@ -1,22 +1,55 @@
// ignore_for_file: comment_references
part of 'spaceship_ramp_cubit.dart'; part of 'spaceship_ramp_cubit.dart';
class SpaceshipRampState extends Equatable { class SpaceshipRampState extends Equatable {
const SpaceshipRampState({ const SpaceshipRampState({
required this.hits, required this.hits,
required this.lightState,
}) : assert(hits >= 0, "Hits can't be negative"); }) : assert(hits >= 0, "Hits can't be negative");
const SpaceshipRampState.initial() : this(hits: 0); const SpaceshipRampState.initial()
: this(
hits: 0,
lightState: ArrowLightState.inactive,
);
final int hits; final int hits;
final ArrowLightState lightState;
bool get arrowFullyLit => lightState == ArrowLightState.active5;
SpaceshipRampState copyWith({ SpaceshipRampState copyWith({
int? hits, int? hits,
ArrowLightState? lightState,
}) { }) {
return SpaceshipRampState( return SpaceshipRampState(
hits: hits ?? this.hits, hits: hits ?? this.hits,
lightState: lightState ?? this.lightState,
); );
} }
@override @override
List<Object?> get props => [hits]; List<Object?> get props => [hits, lightState];
}
/// Indicates the state of the arrow on the [SpaceshipRamp].
enum ArrowLightState {
/// Arrow with no lights lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
} }

@ -1,6 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/gen/assets.gen.dart';
@ -19,30 +20,32 @@ class SpaceshipRamp extends Component {
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
children: children, children: children,
bloc: SpaceshipRampCubit(),
); );
SpaceshipRamp._({ SpaceshipRamp._({
Iterable<Component>? children, Iterable<Component>? children,
required this.bloc,
}) : super( }) : super(
children: [ children: [
_SpaceshipRampOpening( FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>(
outsideLayer: Layer.spaceship, create: SpaceshipRampCubit.new,
outsidePriority: ZIndexes.ballOnSpaceship, children: [
rotation: math.pi, _SpaceshipRampOpening(
) outsideLayer: Layer.spaceship,
..initialPosition = Vector2(-13.7, -18.6) outsidePriority: ZIndexes.ballOnSpaceship,
..layer = Layer.spaceshipEntranceRamp, rotation: math.pi,
_SpaceshipRampBackground(), )
SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5), ..initialPosition = Vector2(-13.7, -18.6)
_SpaceshipRampForegroundRailing(), ..layer = Layer.spaceshipEntranceRamp,
SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), _SpaceshipRampBackground(),
_SpaceshipRampBackgroundRailingSpriteComponent(), SpaceshipRampBoardOpening()
SpaceshipRampArrowSpriteComponent( ..initialPosition = Vector2(3.4, -39.5),
current: bloc.state.hits, _SpaceshipRampForegroundRailing(),
SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5),
_SpaceshipRampBackgroundRailingSpriteComponent(),
SpaceshipRampArrowSpriteComponent(),
...?children,
],
), ),
...?children,
], ],
); );
@ -51,16 +54,8 @@ class SpaceshipRamp extends Component {
/// This can be used for testing [SpaceshipRamp]'s behaviors in isolation. /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation.
@visibleForTesting @visibleForTesting
SpaceshipRamp.test({ SpaceshipRamp.test({
required this.bloc, Iterable<Component>? children,
}) : super(); }) : super(children: children);
final SpaceshipRampCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
} }
class _SpaceshipRampBackground extends BodyComponent class _SpaceshipRampBackground extends BodyComponent
@ -167,82 +162,71 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
/// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
@visibleForTesting @visibleForTesting
class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent<int> class SpaceshipRampArrowSpriteComponent
with HasGameRef, ParentIsA<SpaceshipRamp>, ZIndex { extends SpriteGroupComponent<ArrowLightState>
with
HasGameRef,
ZIndex,
FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
/// {@macro spaceship_ramp_arrow_sprite_component} /// {@macro spaceship_ramp_arrow_sprite_component}
SpaceshipRampArrowSpriteComponent({ SpaceshipRampArrowSpriteComponent()
required int current, : super(
}) : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-3.9, -56.5), position: Vector2(-3.9, -56.5),
current: current,
) { ) {
zIndex = ZIndexes.spaceshipRampArrow; zIndex = ZIndexes.spaceshipRampArrow;
} }
@override
bool listenWhen(
SpaceshipRampState previousState,
SpaceshipRampState newState,
) {
return previousState.lightState != newState.lightState;
}
@override
void onNewState(SpaceshipRampState state) {
current = state.lightState;
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
parent.bloc.stream.listen((state) { final sprites = <ArrowLightState, Sprite>{};
current = state.hits % SpaceshipRampArrowSpriteState.values.length;
});
final sprites = <int, Sprite>{};
this.sprites = sprites; this.sprites = sprites;
for (final spriteState in SpaceshipRampArrowSpriteState.values) { for (final spriteState in ArrowLightState.values) {
sprites[spriteState.index] = Sprite( sprites[spriteState] = Sprite(
gameRef.images.fromCache(spriteState.path), gameRef.images.fromCache(spriteState.path),
); );
} }
current = 0; current = ArrowLightState.inactive;
size = sprites[current]!.originalSize / 10; size = sprites[current]!.originalSize / 10;
} }
} }
/// Indicates the state of the arrow on the [SpaceshipRamp]. extension on ArrowLightState {
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState {
String get path { String get path {
switch (this) { switch (this) {
case SpaceshipRampArrowSpriteState.inactive: case ArrowLightState.inactive:
return Assets.images.android.ramp.arrow.inactive.keyName; return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1: case ArrowLightState.active1:
return Assets.images.android.ramp.arrow.active1.keyName; return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2: case ArrowLightState.active2:
return Assets.images.android.ramp.arrow.active2.keyName; return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3: case ArrowLightState.active3:
return Assets.images.android.ramp.arrow.active3.keyName; return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4: case ArrowLightState.active4:
return Assets.images.android.ramp.arrow.active4.keyName; return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5: case ArrowLightState.active5:
return Assets.images.android.ramp.arrow.active5.keyName; return Assets.images.android.ramp.arrow.active5.keyName;
} }
} }
} }
class SpaceshipRampBoardOpening extends BodyComponent class SpaceshipRampBoardOpening extends BodyComponent
with Layered, ZIndex, InitialPosition, ParentIsA<SpaceshipRamp> { with Layered, ZIndex, InitialPosition {
SpaceshipRampBoardOpening() SpaceshipRampBoardOpening()
: super( : super(
renderBody: false, renderBody: false,

@ -18,6 +18,8 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
intl: ^0.17.0 intl: ^0.17.0
pinball_audio:
path: ../pinball_audio
pinball_flame: pinball_flame:
path: ../pinball_flame path: ../pinball_flame
pinball_theme: pinball_theme:

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
@ -29,11 +30,14 @@ class AndroidSpaceshipGame extends BallGame {
await super.onLoad(); await super.onLoad();
camera.followVector2(Vector2.zero()); camera.followVector2(Vector2.zero());
await addAll( await add(
[ FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>(
AndroidSpaceship(position: Vector2.zero()), create: AndroidSpaceshipCubit.new,
AndroidAnimatronic(), children: [
], AndroidSpaceship(position: Vector2.zero()),
AndroidAnimatronic(),
],
),
); );
await traceAllBodies(); await traceAllBodies();

@ -44,9 +44,9 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
await super.onLoad(); await super.onLoad();
camera.followVector2(Vector2(-12, -50)); camera.followVector2(Vector2(-12, -50));
await add(
_spaceshipRamp = SpaceshipRamp(), _spaceshipRamp = SpaceshipRamp();
); await add(_spaceshipRamp);
await traceAllBodies(); await traceAllBodies();
} }
@ -57,7 +57,9 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
) { ) {
if (event is RawKeyDownEvent && if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) { event.logicalKey == LogicalKeyboardKey.space) {
_spaceshipRamp.bloc.onAscendingBallEntered(); _spaceshipRamp
.readBloc<SpaceshipRampCubit, SpaceshipRampState>()
.onProgressed();
return KeyEventResult.handled; return KeyEventResult.handled;
} }

@ -10,6 +10,11 @@ class LaunchRampGame extends BallGame {
: super( : super(
ballPriority: ZIndexes.ballOnLaunchRamp, ballPriority: ZIndexes.ballOnLaunchRamp,
ballLayer: Layer.launcher, ballLayer: Layer.launcher,
imagesFileNames: [
Assets.images.launchRamp.ramp.keyName,
Assets.images.launchRamp.backgroundRailing.keyName,
Assets.images.launchRamp.foregroundRailing.keyName,
],
); );
static const description = ''' static const description = '''

@ -1,11 +1,18 @@
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flutter/material.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class PlungerGame extends BallGame with KeyboardEvents, Traceable { class PlungerGame extends BallGame
with HasKeyboardHandlerComponents, Traceable {
PlungerGame()
: super(
imagesFileNames: [
Assets.images.plunger.plunger.keyName,
],
);
static const description = ''' static const description = '''
Shows how Plunger is rendered. Shows how Plunger is rendered.
@ -13,39 +20,21 @@ class PlungerGame extends BallGame with KeyboardEvents, Traceable {
- Tap anywhere on the screen to spawn a ball into the game. - Tap anywhere on the screen to spawn a ball into the game.
'''; ''';
static const _downKeys = [
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
];
late Plunger plunger;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2); final center = screenToWorld(camera.viewport.canvasSize! / 2);
final plunger = Plunger()
..initialPosition = Vector2(center.x - 8.8, center.y);
await add( await add(
plunger = Plunger(compressionDistance: 29) FlameBlocProvider<PlungerCubit, PlungerState>(
..initialPosition = Vector2(center.x - 8.8, center.y), create: PlungerCubit.new,
children: [plunger],
),
); );
await traceAllBodies(); await plunger.add(PlungerKeyControllingBehavior());
}
@override await traceAllBodies();
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
final movedPlungerDown = _downKeys.contains(event.logicalKey);
if (movedPlungerDown) {
if (event is RawKeyDownEvent) {
plunger.pull();
} else if (event is RawKeyUpEvent) {
plunger.release();
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
} }

@ -15,6 +15,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.2" version: "2.8.2"
audioplayers:
dependency: transitive
description:
name: audioplayers
url: "https://pub.dartlang.org"
source: hosted
version: "0.20.1"
bloc: bloc:
dependency: transitive dependency: transitive
description: description:
@ -57,6 +64,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.15.0" version: "1.15.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
dashbook: dashbook:
dependency: "direct main" dependency: "direct main"
description: description:
@ -106,8 +120,15 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
flame_bloc: flame_audio:
dependency: transitive dependency: transitive
description:
name: flame_audio
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
flame_bloc:
dependency: "direct main"
description: description:
name: flame_bloc name: flame_bloc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@ -179,6 +200,20 @@ packages:
relative: true relative: true
source: path source: path
version: "1.0.0+1" version: "1.0.0+1"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.4"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@ -249,6 +284,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
path_provider:
dependency: transitive
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -256,6 +312,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.5" version: "2.1.5"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -270,6 +333,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.5"
pinball_audio:
dependency: transitive
description:
path: "../../pinball_audio"
relative: true
source: path
version: "1.0.0+1"
pinball_components: pinball_components:
dependency: "direct main" dependency: "direct main"
description: description:
@ -415,6 +485,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
synchronized:
dependency: transitive
description:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0+2"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -492,6 +569,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

@ -9,6 +9,7 @@ environment:
dependencies: dependencies:
dashbook: ^0.1.7 dashbook: ^0.1.7
flame: ^1.1.1 flame: ^1.1.1
flame_bloc: ^1.4.0
flame_forge2d: flame_forge2d:
git: git:
url: https://github.com/flame-engine/flame url: https://github.com/flame-engine/flame

@ -7,7 +7,7 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart'; import 'package:pinball_components/src/components/android_animatronic/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
@ -23,19 +23,19 @@ void main() {
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
group( group(
'AndroidSpaceshipEntranceBallContactBehavior', 'AndroidAnimatronicBallContactBehavior',
() { () {
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
AndroidSpaceshipEntranceBallContactBehavior(), AndroidAnimatronicBallContactBehavior(),
isA<AndroidSpaceshipEntranceBallContactBehavior>(), isA<AndroidAnimatronicBallContactBehavior>(),
); );
}); });
flameTester.test( flameTester.test(
'beginContact calls onBallEntered when entrance contacts with a ball', 'beginContact calls onBallContacted when in contact with a ball',
(game) async { (game) async {
final behavior = AndroidSpaceshipEntranceBallContactBehavior(); final behavior = AndroidAnimatronicBallContactBehavior();
final bloc = _MockAndroidSpaceshipCubit(); final bloc = _MockAndroidSpaceshipCubit();
whenListen( whenListen(
bloc, bloc,
@ -43,20 +43,20 @@ void main() {
initialState: AndroidSpaceshipState.withoutBonus, initialState: AndroidSpaceshipState.withoutBonus,
); );
final entrance = AndroidSpaceshipEntrance(); final animatronic = AndroidAnimatronic.test();
final androidSpaceship = FlameBlocProvider<AndroidSpaceshipCubit, final androidSpaceship = FlameBlocProvider<AndroidSpaceshipCubit,
AndroidSpaceshipState>.value( AndroidSpaceshipState>.value(
value: bloc, value: bloc,
children: [ children: [
AndroidSpaceship.test(children: [entrance]) AndroidSpaceship.test(children: [animatronic])
], ],
); );
await entrance.add(behavior); await animatronic.add(behavior);
await game.ensureAdd(androidSpaceship); await game.ensureAdd(androidSpaceship);
behavior.beginContact(_MockBall(), _MockContact()); behavior.beginContact(_MockBall(), _MockContact());
verify(bloc.onBallEntered).called(1); verify(bloc.onBallContacted).called(1);
}, },
); );
}, },

@ -4,6 +4,7 @@ import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/android_animatronic/behaviors/behaviors.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -58,13 +59,26 @@ void main() {
}, },
); );
flameTester.test('adds new children', (game) async { group('adds', () {
final component = Component(); flameTester.test('new children', (game) async {
final androidAnimatronic = AndroidAnimatronic( final component = Component();
children: [component], final androidAnimatronic = AndroidAnimatronic(
); children: [component],
await game.ensureAdd(androidAnimatronic); );
expect(androidAnimatronic.children, contains(component)); await game.ensureAdd(androidAnimatronic);
expect(androidAnimatronic.children, contains(component));
});
flameTester.test('a AndroidAnimatronicBallContactBehavior', (game) async {
final androidAnimatronic = AndroidAnimatronic();
await game.ensureAdd(androidAnimatronic);
expect(
androidAnimatronic.children
.whereType<AndroidAnimatronicBallContactBehavior>()
.single,
isNotNull,
);
});
}); });
}); });
} }

@ -6,7 +6,6 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
@ -84,26 +83,5 @@ void main() {
); );
}, },
); );
flameTester.test(
'AndroidSpaceshipEntrance has an '
'AndroidSpaceshipEntranceBallContactBehavior', (game) async {
final androidSpaceship = AndroidSpaceship(position: Vector2.zero());
final provider =
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>.value(
value: bloc,
children: [androidSpaceship],
);
await game.ensureAdd(provider);
final androidSpaceshipEntrance =
androidSpaceship.firstChild<AndroidSpaceshipEntrance>();
expect(
androidSpaceshipEntrance!.children
.whereType<AndroidSpaceshipEntranceBallContactBehavior>()
.single,
isNotNull,
);
});
}); });
} }

@ -9,7 +9,7 @@ void main() {
blocTest<AndroidSpaceshipCubit, AndroidSpaceshipState>( blocTest<AndroidSpaceshipCubit, AndroidSpaceshipState>(
'onBallEntered emits withBonus', 'onBallEntered emits withBonus',
build: AndroidSpaceshipCubit.new, build: AndroidSpaceshipCubit.new,
act: (bloc) => bloc.onBallEntered(), act: (bloc) => bloc.onBallContacted(),
expect: () => [AndroidSpaceshipState.withBonus], expect: () => [AndroidSpaceshipState.withBonus],
); );

@ -14,8 +14,11 @@ void main() {
group( group(
'BallTurboChargingBehavior', 'BallTurboChargingBehavior',
() { () {
final asset = theme.Assets.images.dash.ball.keyName; final assets = [
final flameTester = FlameTester(() => TestGame([asset])); theme.Assets.images.dash.ball.keyName,
Assets.images.ball.flameEffect.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(

@ -24,6 +24,20 @@ void main() {
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
group('BumpingBehavior', () { group('BumpingBehavior', () {
test('can be instantiated', () {
expect(
BumpingBehavior(strength: 0),
isA<BumpingBehavior>(),
);
});
test('throws assertion error when strength is negative ', () {
expect(
() => BumpingBehavior(strength: -1),
throwsAssertionError,
);
});
flameTester.test('can be added', (game) async { flameTester.test('can be added', (game) async {
final behavior = BumpingBehavior(strength: 0); final behavior = BumpingBehavior(strength: 0);
final component = _TestBodyComponent(); final component = _TestBodyComponent();

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

@ -1,5 +1,8 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -8,7 +11,24 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart'; class _TestGame extends Forge2DGame {
Future<void> pump(
FlipperKeyControllingBehavior behavior, {
required BoardSide side,
FlipperCubit? flipperBloc,
}) async {
final flipper = Flipper.test(side: side);
await ensureAdd(flipper);
await flipper.ensureAdd(
FlameBlocProvider<FlipperCubit, FlipperState>.value(
value: flipperBloc ?? FlipperCubit(),
children: [behavior],
),
);
}
}
class _MockFlipperCubit extends Mock implements FlipperCubit {}
class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@override @override
@ -27,26 +47,32 @@ class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
group('FlipperKeyControllingBehavior', () { group('FlipperKeyControllingBehavior', () {
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(_TestGame.new);
group( group(
'onKeyEvent', 'onKeyEvent',
() { () {
late Flipper rightFlipper; late FlipperCubit flipperBloc;
late Flipper leftFlipper;
setUp(() { setUp(() {
rightFlipper = Flipper.test(side: BoardSide.right); flipperBloc = _MockFlipperCubit();
leftFlipper = Flipper.test(side: BoardSide.left); whenListen<FlipperState>(
flipperBloc,
const Stream.empty(),
initialState: FlipperState.movingDown,
);
}); });
group('on right Flipper', () { group('on right Flipper', () {
flameTester.test( flameTester.test(
'moves upwards when right arrow is pressed', 'moves upwards when right arrow is pressed',
(game) async { (game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior); await game.pump(
behavior,
flipperBloc: flipperBloc,
side: BoardSide.right,
);
final event = _MockRawKeyDownEvent(); final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -55,17 +81,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isNegative); await Future<void>.delayed(Duration.zero);
expect(rightFlipper.body.linearVelocity.x, isZero); verify(flipperBloc.moveUp).called(1);
}, },
); );
flameTester.test( flameTester.test(
'moves downwards when right arrow is released', 'moves downwards when right arrow is released',
(game) async { (game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.right,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyUpEvent(); final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -74,17 +103,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isPositive); await Future<void>.delayed(Duration.zero);
expect(rightFlipper.body.linearVelocity.x, isZero); verify(flipperBloc.moveDown).called(1);
}, },
); );
flameTester.test( flameTester.test(
'moves upwards when D is pressed', 'moves upwards when D is pressed',
(game) async { (game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.right,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyDownEvent(); final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -93,17 +125,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isNegative); await Future<void>.delayed(Duration.zero);
expect(rightFlipper.body.linearVelocity.x, isZero); verify(flipperBloc.moveUp).called(1);
}, },
); );
flameTester.test( flameTester.test(
'moves downwards when D is released', 'moves downwards when D is released',
(game) async { (game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.right,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyUpEvent(); final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -112,8 +147,8 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isPositive); await Future<void>.delayed(Duration.zero);
expect(rightFlipper.body.linearVelocity.x, isZero); verify(flipperBloc.moveDown).called(1);
}, },
); );
@ -121,9 +156,12 @@ void main() {
flameTester.test( flameTester.test(
'left arrow is pressed', 'left arrow is pressed',
(game) async { (game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.right,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyDownEvent(); final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -132,17 +170,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero); verifyNever(flipperBloc.moveDown);
expect(rightFlipper.body.linearVelocity.x, isZero); verifyNever(flipperBloc.moveUp);
}, },
); );
flameTester.test( flameTester.test(
'left arrow is released', 'left arrow is released',
(game) async { (game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.right,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyUpEvent(); final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -151,17 +192,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero); verifyNever(flipperBloc.moveDown);
expect(rightFlipper.body.linearVelocity.x, isZero); verifyNever(flipperBloc.moveUp);
}, },
); );
flameTester.test( flameTester.test(
'A is pressed', 'A is pressed',
(game) async { (game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.right,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyDownEvent(); final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -170,17 +214,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero); verifyNever(flipperBloc.moveDown);
expect(rightFlipper.body.linearVelocity.x, isZero); verifyNever(flipperBloc.moveUp);
}, },
); );
flameTester.test( flameTester.test(
'A is released', 'A is released',
(game) async { (game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.right,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyUpEvent(); final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -189,8 +236,8 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero); verifyNever(flipperBloc.moveDown);
expect(rightFlipper.body.linearVelocity.x, isZero); verifyNever(flipperBloc.moveUp);
}, },
); );
}); });
@ -200,9 +247,12 @@ void main() {
flameTester.test( flameTester.test(
'moves upwards when left arrow is pressed', 'moves upwards when left arrow is pressed',
(game) async { (game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.left,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyDownEvent(); final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -211,17 +261,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isNegative); await Future<void>.delayed(Duration.zero);
expect(leftFlipper.body.linearVelocity.x, isZero); verify(flipperBloc.moveUp).called(1);
}, },
); );
flameTester.test( flameTester.test(
'moves downwards when left arrow is released', 'moves downwards when left arrow is released',
(game) async { (game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.left,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyUpEvent(); final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -230,17 +283,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isPositive); await Future<void>.delayed(Duration.zero);
expect(leftFlipper.body.linearVelocity.x, isZero); verify(flipperBloc.moveDown).called(1);
}, },
); );
flameTester.test( flameTester.test(
'moves upwards when A is pressed', 'moves upwards when A is pressed',
(game) async { (game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.left,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyDownEvent(); final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -249,17 +305,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isNegative); await Future<void>.delayed(Duration.zero);
expect(leftFlipper.body.linearVelocity.x, isZero); verify(flipperBloc.moveUp).called(1);
}, },
); );
flameTester.test( flameTester.test(
'moves downwards when A is released', 'moves downwards when A is released',
(game) async { (game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.left,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyUpEvent(); final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -268,8 +327,8 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isPositive); await Future<void>.delayed(Duration.zero);
expect(leftFlipper.body.linearVelocity.x, isZero); verify(flipperBloc.moveDown).called(1);
}, },
); );
@ -277,9 +336,12 @@ void main() {
flameTester.test( flameTester.test(
'right arrow is pressed', 'right arrow is pressed',
(game) async { (game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.left,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyDownEvent(); final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -288,17 +350,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero); verifyNever(flipperBloc.moveDown);
expect(leftFlipper.body.linearVelocity.x, isZero); verifyNever(flipperBloc.moveUp);
}, },
); );
flameTester.test( flameTester.test(
'right arrow is released', 'right arrow is released',
(game) async { (game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.left,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyUpEvent(); final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -307,17 +372,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero); verifyNever(flipperBloc.moveDown);
expect(leftFlipper.body.linearVelocity.x, isZero); verifyNever(flipperBloc.moveUp);
}, },
); );
flameTester.test( flameTester.test(
'D is pressed', 'D is pressed',
(game) async { (game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.left,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyDownEvent(); final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -326,17 +394,20 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero); verifyNever(flipperBloc.moveDown);
expect(leftFlipper.body.linearVelocity.x, isZero); verifyNever(flipperBloc.moveUp);
}, },
); );
flameTester.test( flameTester.test(
'D is released', 'D is released',
(game) async { (game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior(); final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior); await game.pump(
behavior,
side: BoardSide.left,
flipperBloc: flipperBloc,
);
final event = _MockRawKeyUpEvent(); final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
@ -345,8 +416,8 @@ void main() {
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero); verifyNever(flipperBloc.moveDown);
expect(leftFlipper.body.linearVelocity.x, isZero); verifyNever(flipperBloc.moveUp);
}, },
); );
}); });

@ -0,0 +1,101 @@
// ignore_for_file: avoid_dynamic_calls, cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
FlipperMovingBehavior behavior, {
FlipperCubit? flipperBloc,
}) async {
final flipper = Flipper.test(side: BoardSide.left);
await ensureAdd(flipper);
await flipper.ensureAdd(
FlameBlocProvider<FlipperCubit, FlipperState>.value(
value: flipperBloc ?? FlipperCubit(),
children: [behavior],
),
);
}
}
class _MockFlipperCubit extends Mock implements FlipperCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('FlipperMovingBehavior', () {
test('can be instantiated', () {
expect(
FlipperMovingBehavior(strength: 0),
isA<FlipperMovingBehavior>(),
);
});
test('throws assertion error when strength is negative', () {
expect(
() => FlipperMovingBehavior(strength: -1),
throwsAssertionError,
);
});
flameTester.test('can be loaded', (game) async {
final behavior = FlipperMovingBehavior(strength: 0);
await game.pump(behavior);
expect(game.descendants(), contains(behavior));
});
flameTester.test(
'applies vertical velocity to flipper when moving down',
(game) async {
final bloc = _MockFlipperCubit();
final streamController = StreamController<FlipperState>();
whenListen(
bloc,
streamController.stream,
initialState: FlipperState.movingUp,
);
const strength = 10.0;
final behavior = FlipperMovingBehavior(strength: strength);
await game.pump(behavior, flipperBloc: bloc);
streamController.add(FlipperState.movingDown);
await Future<void>.delayed(Duration.zero);
final flipper = behavior.ancestors().whereType<Flipper>().single;
expect(flipper.body.linearVelocity.x, 0);
expect(flipper.body.linearVelocity.y, strength);
},
);
flameTester.test(
'applies vertical velocity to flipper when moving up',
(game) async {
final bloc = _MockFlipperCubit();
whenListen(
bloc,
Stream.value(FlipperState.movingUp),
initialState: FlipperState.movingUp,
);
const strength = 10.0;
final behavior = FlipperMovingBehavior(strength: strength);
await game.pump(behavior, flipperBloc: bloc);
game.update(0);
final flipper = behavior.ancestors().whereType<Flipper>().single;
expect(flipper.body.linearVelocity.x, 0);
expect(flipper.body.linearVelocity.y, -strength);
},
);
});
}

@ -0,0 +1,23 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('FlipperCubit', () {
test('can be instantiated', () {
expect(FlipperCubit(), isA<FlipperCubit>());
});
blocTest<FlipperCubit, FlipperState>(
'moves',
build: FlipperCubit.new,
act: (cubit) => cubit
..moveUp()
..moveDown(),
expect: () => [
FlipperState.movingUp,
FlipperState.movingDown,
],
);
});
}

@ -128,31 +128,5 @@ void main() {
}, },
); );
}); });
flameTester.test(
'moveDown applies downward velocity',
(game) async {
final flipper = Flipper(side: BoardSide.left);
await game.ensureAdd(flipper);
expect(flipper.body.linearVelocity, equals(Vector2.zero()));
flipper.moveDown();
expect(flipper.body.linearVelocity.y, isPositive);
},
);
flameTester.test(
'moveUp applies upward velocity',
(game) async {
final flipper = Flipper(side: BoardSide.left);
await game.ensureAdd(flipper);
expect(flipper.body.linearVelocity, equals(Vector2.zero()));
flipper.moveUp();
expect(flipper.body.linearVelocity.y, isNegative);
},
);
}); });
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

@ -0,0 +1,64 @@
// ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
GoogleWordAnimatingBehavior child, {
required GoogleWordCubit bloc,
}) async {
await ensureAdd(
FlameBlocProvider<GoogleWordCubit, GoogleWordState>.value(
value: bloc,
children: [child],
),
);
}
}
class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('GoogleWordAnimatingBehavior', () {
flameTester.testGameWidget(
'calls switched after timer period reached',
setUp: (game, tester) async {
final behavior = GoogleWordAnimatingBehavior();
final bloc = _MockGoogleWordCubit();
await game.pump(behavior, bloc: bloc);
game.update(behavior.timer.limit);
verify(bloc.switched).called(1);
},
);
flameTester.testGameWidget(
'calls onReset and removes itself '
'after all blinks complete',
setUp: (game, tester) async {
final behavior = GoogleWordAnimatingBehavior();
final bloc = _MockGoogleWordCubit();
await game.pump(behavior, bloc: bloc);
for (var i = 0; i <= 14; i++) {
game.update(behavior.timer.limit);
}
await game.ready();
verify(bloc.onReset).called(1);
expect(
game.descendants().whereType<GoogleWordAnimatingBehavior>().isEmpty,
isTrue,
);
},
);
});
}

@ -6,6 +6,21 @@ void main() {
group( group(
'GoogleWordCubit', 'GoogleWordCubit',
() { () {
final litEvens = {
for (int i = 0; i < 6; i++)
if (i.isEven)
i: GoogleLetterSpriteState.lit
else
i: GoogleLetterSpriteState.dimmed
};
final litOdds = {
for (int i = 0; i < 6; i++)
if (i.isOdd)
i: GoogleLetterSpriteState.lit
else
i: GoogleLetterSpriteState.dimmed
};
blocTest<GoogleWordCubit, GoogleWordState>( blocTest<GoogleWordCubit, GoogleWordState>(
'onRolloverContacted emits first letter lit', 'onRolloverContacted emits first letter lit',
build: GoogleWordCubit.new, build: GoogleWordCubit.new,
@ -25,9 +40,31 @@ void main() {
); );
blocTest<GoogleWordCubit, GoogleWordState>( blocTest<GoogleWordCubit, GoogleWordState>(
'onBonusAwarded emits initial state', 'switched emits all even letters lit when first letter is dimmed',
build: GoogleWordCubit.new,
act: (bloc) => bloc.switched(),
expect: () => [GoogleWordState(letterSpriteStates: litEvens)],
);
blocTest<GoogleWordCubit, GoogleWordState>(
'switched emits all odd letters lit when first letter is lit',
build: GoogleWordCubit.new,
seed: () => GoogleWordState(letterSpriteStates: litEvens),
act: (bloc) => bloc.switched(),
expect: () => [GoogleWordState(letterSpriteStates: litOdds)],
);
blocTest<GoogleWordCubit, GoogleWordState>(
'onBonusAwarded emits all even letters lit',
build: GoogleWordCubit.new, build: GoogleWordCubit.new,
act: (bloc) => bloc.onBonusAwarded(), act: (bloc) => bloc.onBonusAwarded(),
expect: () => [GoogleWordState(letterSpriteStates: litEvens)],
);
blocTest<GoogleWordCubit, GoogleWordState>(
'onReset emits initial state',
build: GoogleWordCubit.new,
act: (bloc) => bloc.onReset(),
expect: () => [GoogleWordState.initial()], expect: () => [GoogleWordState.initial()],
); );
}, },

@ -0,0 +1,36 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
group('PlungerJointingBehavior', () {
test('can be instantiated', () {
expect(
PlungerJointingBehavior(compressionDistance: 0),
isA<PlungerJointingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final parent = Plunger.test();
final behavior = PlungerJointingBehavior(compressionDistance: 0);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
expect(parent.children, contains(behavior));
});
flameTester.test('creates a joint', (game) async {
final behavior = PlungerJointingBehavior(compressionDistance: 0);
final parent = Plunger.test();
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
expect(parent.body.joints, isNotEmpty);
});
});
}

@ -0,0 +1,194 @@
// ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/foundation.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';
class _TestGame extends Forge2DGame {
Future<void> pump(
PlungerKeyControllingBehavior child, {
PlungerCubit? plungerBloc,
}) async {
final plunger = Plunger.test();
await ensureAdd(plunger);
return plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plungerBloc ?? _MockPlungerCubit(),
children: [child],
),
);
}
}
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();
}
}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('PlungerKeyControllingBehavior', () {
test('can be instantiated', () {
expect(
PlungerKeyControllingBehavior(),
isA<PlungerKeyControllingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(behavior);
expect(game.descendants(), contains(behavior));
});
group('onKeyEvent', () {
late PlungerCubit plungerBloc;
setUp(() {
plungerBloc = _MockPlungerCubit();
});
group('pulls when', () {
flameTester.test(
'down arrow is pressed',
(game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowDown,
);
behavior.onKeyEvent(event, {});
verify(() => plungerBloc.pulled()).called(1);
},
);
flameTester.test(
'"s" is pressed',
(game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyS,
);
behavior.onKeyEvent(event, {});
verify(() => plungerBloc.pulled()).called(1);
},
);
flameTester.test(
'space is pressed',
(game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.space,
);
behavior.onKeyEvent(event, {});
verify(() => plungerBloc.pulled()).called(1);
},
);
});
group('releases when', () {
flameTester.test(
'down arrow is released',
(game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowDown,
);
behavior.onKeyEvent(event, {});
verify(() => plungerBloc.released()).called(1);
},
);
flameTester.test(
'"s" is released',
(game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyS,
);
behavior.onKeyEvent(event, {});
verify(() => plungerBloc.released()).called(1);
},
);
flameTester.test(
'space is released',
(game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.space,
);
behavior.onKeyEvent(event, {});
verify(() => plungerBloc.released()).called(1);
},
);
});
});
});
}

@ -0,0 +1,91 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
Component child, {
PinballAudioPlayer? pinballAudioPlayer,
PlungerCubit? plungerBloc,
}) async {
final parent = Component();
await ensureAdd(parent);
return parent.ensureAdd(
FlameProvider<PinballAudioPlayer>.value(
pinballAudioPlayer ?? _MockPinballAudioPlayer(),
children: [
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plungerBloc ?? PlungerCubit(),
children: [child],
),
],
),
);
}
}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('PlungerNoiseBehavior', () {
late PinballAudioPlayer audioPlayer;
setUp(() {
audioPlayer = _MockPinballAudioPlayer();
});
test('can be instantiated', () {
expect(
PlungerNoiseBehavior(),
isA<PlungerNoiseBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final behavior = PlungerNoiseBehavior();
await game.pump(behavior);
expect(game.descendants(), contains(behavior));
});
flameTester.test(
'plays the correct sound when released',
(game) async {
final plungerBloc = _MockPlungerCubit();
final streamController = StreamController<PlungerState>();
whenListen<PlungerState>(
plungerBloc,
streamController.stream,
initialState: PlungerState.pulling,
);
final behavior = PlungerNoiseBehavior();
await game.pump(
behavior,
pinballAudioPlayer: audioPlayer,
plungerBloc: plungerBloc,
);
streamController.add(PlungerState.releasing);
await Future<void>.delayed(Duration.zero);
verify(() => audioPlayer.play(PinballAudio.launcher)).called(1);
},
);
});
}

@ -0,0 +1,160 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
PlungerPullingBehavior behavior, {
PlungerCubit? plungerBloc,
}) async {
final plunger = Plunger.test();
await ensureAdd(plunger);
return plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plungerBloc ?? _MockPlungerCubit(),
children: [behavior],
),
);
}
}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
class _MockPrismaticJoint extends Mock implements PrismaticJoint {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('PlungerPullingBehavior', () {
test('can be instantiated', () {
expect(
PlungerPullingBehavior(strength: 0),
isA<PlungerPullingBehavior>(),
);
});
test('throws assertion error when strength is negative ', () {
expect(
() => PlungerPullingBehavior(strength: -1),
throwsAssertionError,
);
});
flameTester.test('can be loaded', (game) async {
final behavior = PlungerPullingBehavior(strength: 0);
await game.pump(behavior);
expect(game.descendants(), contains(behavior));
});
flameTester.test(
'applies vertical linear velocity when pulled',
(game) async {
final plungerBloc = _MockPlungerCubit();
whenListen<PlungerState>(
plungerBloc,
Stream.value(PlungerState.pulling),
initialState: PlungerState.pulling,
);
const strength = 2.0;
final behavior = PlungerPullingBehavior(
strength: strength,
);
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
game.update(0);
final plunger = behavior.ancestors().whereType<Plunger>().single;
expect(plunger.body.linearVelocity.x, equals(0));
expect(plunger.body.linearVelocity.y, equals(strength));
},
);
});
group('PlungerAutoPullingBehavior', () {
test('can be instantiated', () {
expect(
PlungerAutoPullingBehavior(strength: 0),
isA<PlungerAutoPullingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final behavior = PlungerAutoPullingBehavior(strength: 0);
await game.pump(behavior);
expect(game.descendants(), contains(behavior));
});
flameTester.test(
"pulls while joint hasn't reached limit",
(game) async {
final plungerBloc = _MockPlungerCubit();
whenListen<PlungerState>(
plungerBloc,
Stream.value(PlungerState.pulling),
initialState: PlungerState.pulling,
);
const strength = 2.0;
final behavior = PlungerAutoPullingBehavior(
strength: strength,
);
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final plunger = behavior.ancestors().whereType<Plunger>().single;
final joint = _MockPrismaticJoint();
when(joint.getJointTranslation).thenReturn(2);
when(joint.getLowerLimit).thenReturn(0);
plunger.body.joints.add(joint);
game.update(0);
expect(plunger.body.linearVelocity.x, equals(0));
expect(plunger.body.linearVelocity.y, equals(strength));
},
);
flameTester.test(
'releases when joint reaches limit',
(game) async {
final plungerBloc = _MockPlungerCubit();
whenListen<PlungerState>(
plungerBloc,
Stream.value(PlungerState.pulling),
initialState: PlungerState.pulling,
);
const strength = 2.0;
final behavior = PlungerAutoPullingBehavior(
strength: strength,
);
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final plunger = behavior.ancestors().whereType<Plunger>().single;
final joint = _MockPrismaticJoint();
when(joint.getJointTranslation).thenReturn(0);
when(joint.getLowerLimit).thenReturn(0);
plunger.body.joints.add(joint);
game.update(0);
verify(plungerBloc.released).called(1);
},
);
});
}

@ -0,0 +1,79 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/forge2d_game.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';
class _TestGame extends Forge2DGame {
Future<void> pump(
PlungerReleasingBehavior behavior, {
PlungerCubit? plungerBloc,
}) async {
final plunger = Plunger.test();
await ensureAdd(plunger);
return plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plungerBloc ?? PlungerCubit(),
children: [behavior],
),
);
}
}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
void main() {
group('PlungerReleasingBehavior', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
test('can be instantiated', () {
expect(
PlungerReleasingBehavior(strength: 0),
isA<PlungerReleasingBehavior>(),
);
});
test('throws assertion error when strength is negative ', () {
expect(
() => PlungerReleasingBehavior(strength: -1),
throwsAssertionError,
);
});
flameTester.test('can be loaded', (game) async {
final behavior = PlungerReleasingBehavior(strength: 0);
await game.pump(behavior);
expect(game.descendants(), contains(behavior));
});
flameTester.test('applies vertical linear velocity', (game) async {
final plungerBloc = _MockPlungerCubit();
final streamController = StreamController<PlungerState>();
whenListen<PlungerState>(
plungerBloc,
streamController.stream,
initialState: PlungerState.pulling,
);
final behavior = PlungerReleasingBehavior(strength: 2);
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
streamController.add(PlungerState.releasing);
await Future<void>.delayed(Duration.zero);
final plunger = behavior.ancestors().whereType<Plunger>().single;
expect(plunger.body.linearVelocity.x, equals(0));
expect(plunger.body.linearVelocity.y, isNot(greaterThan(0)));
});
});
}

@ -0,0 +1,119 @@
// ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final asset = Assets.images.plunger.plunger.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
group('Plunger', () {
test('can be instantiated', () {
expect(Plunger(), isA<Plunger>());
});
flameTester.test(
'loads correctly',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(game.children, contains(plunger));
},
);
group('adds', () {
flameTester.test(
'a PlungerReleasingBehavior',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(
game.descendants().whereType<PlungerReleasingBehavior>().length,
equals(1),
);
},
);
flameTester.test(
'a PlungerJointingBehavior',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(
game.descendants().whereType<PlungerJointingBehavior>().length,
equals(1),
);
},
);
flameTester.test(
'a PlungerNoiseBehavior',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(
game.descendants().whereType<PlungerNoiseBehavior>().length,
equals(1),
);
},
);
});
group('renders correctly', () {
const goldenPath = '../golden/plunger/';
flameTester.testGameWidget(
'pulling',
setUp: (game, tester) async {
await game.images.load(asset);
await game.ensureAdd(Plunger());
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 4.1;
},
verify: (game, tester) async {
final plunger = game.descendants().whereType<Plunger>().first;
final bloc = plunger
.descendants()
.whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
.single
.bloc;
bloc.pulled();
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenPath}pull.png'),
);
},
);
flameTester.testGameWidget(
'releasing',
setUp: (game, tester) async {
await game.images.load(asset);
await game.ensureAdd(Plunger());
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 4.1;
},
verify: (game, tester) async {
final plunger = game.descendants().whereType<Plunger>().first;
final bloc = plunger
.descendants()
.whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
.single
.bloc;
bloc.released();
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenPath}release.png'),
);
},
);
});
});
}

@ -1,391 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('Plunger', () {
const compressionDistance = 0.0;
test('can be instantiated', () {
expect(
Plunger(compressionDistance: compressionDistance),
isA<Plunger>(),
);
expect(
Plunger.test(compressionDistance: compressionDistance),
isA<Plunger>(),
);
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.ensureAdd(Plunger(compressionDistance: compressionDistance));
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 4.1;
},
verify: (game, tester) async {
final plunger = game.descendants().whereType<Plunger>().first;
plunger.pull();
game.update(1);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/plunger/pull.png'),
);
plunger.release();
game.update(1);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/plunger/release.png'),
);
},
);
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(game.contains(plunger), isTrue);
},
);
group('body', () {
flameTester.test(
'is dynamic',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(plunger.body.bodyType, equals(BodyType.dynamic));
},
);
flameTester.test(
'ignores gravity',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(plunger.body.gravityScale, equals(Vector2.zero()));
},
);
});
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(plunger.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'shape is a polygon',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
final fixture = plunger.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.polygon));
},
);
flameTester.test(
'has density',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
final fixture = plunger.body.fixtures[0];
expect(fixture.density, greaterThan(0));
},
);
});
group('pullFor', () {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
});
flameTester.testGameWidget(
'moves downwards for given period when pullFor is called',
setUp: (game, tester) async {
await game.ensureAdd(plunger);
},
verify: (game, tester) async {
plunger.pullFor(2);
game.update(0);
expect(plunger.body.linearVelocity.y, isPositive);
// Call game update at 120 FPS, so that the plunger will act as if it
// was pulled for 2 seconds.
for (var i = 0.0; i < 2; i += 1 / 120) {
game.update(1 / 20);
}
expect(plunger.body.linearVelocity.y, isZero);
},
);
});
group('pull', () {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
});
flameTester.test(
'moves downwards when pull is called',
(game) async {
await game.ensureAdd(plunger);
plunger.pull();
expect(plunger.body.linearVelocity.y, isPositive);
expect(plunger.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves downwards when pull is called '
'and plunger is below its starting position', (game) async {
await game.ensureAdd(plunger);
plunger.pull();
plunger.release();
plunger.pull();
expect(plunger.body.linearVelocity.y, isPositive);
expect(plunger.body.linearVelocity.x, isZero);
});
});
group('release', () {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
});
flameTester.test(
'moves upwards when release is called '
'and plunger is below its starting position', (game) async {
await game.ensureAdd(plunger);
plunger.body.setTransform(Vector2(0, 1), 0);
plunger.release();
expect(plunger.body.linearVelocity.y, isNegative);
expect(plunger.body.linearVelocity.x, isZero);
});
flameTester.test(
'does not move when release is called '
'and plunger is in its starting position',
(game) async {
await game.ensureAdd(plunger);
plunger.release();
expect(plunger.body.linearVelocity.y, isZero);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
});
group('PlungerAnchor', () {
const compressionDistance = 10.0;
flameTester.test(
'position is a compression distance below the Plunger',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
final plungerAnchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(plungerAnchor);
expect(
plungerAnchor.body.position.y,
equals(plunger.body.position.y + compressionDistance),
);
},
);
});
group('PlungerAnchorPrismaticJointDef', () {
const compressionDistance = 10.0;
late Plunger plunger;
late PlungerAnchor anchor;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
anchor = PlungerAnchor(plunger: plunger);
});
group('initializes with', () {
flameTester.test(
'plunger body as bodyA',
(game) async {
await game.ensureAdd(plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
expect(jointDef.bodyA, equals(plunger.body));
},
);
flameTester.test(
'anchor body as bodyB',
(game) async {
await game.ensureAdd(plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(PrismaticJoint(jointDef));
expect(jointDef.bodyB, equals(anchor.body));
},
);
flameTester.test(
'limits enabled',
(game) async {
await game.ensureAdd(plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(PrismaticJoint(jointDef));
expect(jointDef.enableLimit, isTrue);
},
);
flameTester.test(
'lower translation limit as negative infinity',
(game) async {
await game.ensureAdd(plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(PrismaticJoint(jointDef));
expect(jointDef.lowerTranslation, equals(double.negativeInfinity));
},
);
flameTester.test(
'connected body collision enabled',
(game) async {
await game.ensureAdd(plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(PrismaticJoint(jointDef));
expect(jointDef.collideConnected, isTrue);
},
);
});
flameTester.testGameWidget(
'plunger cannot go below anchor',
setUp: (game, tester) async {
await game.ensureAdd(plunger);
await game.ensureAdd(anchor);
// Giving anchor a shape for the plunger to collide with.
anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1));
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(PrismaticJoint(jointDef));
await tester.pump(const Duration(seconds: 1));
},
verify: (game, tester) async {
expect(plunger.body.position.y < anchor.body.position.y, isTrue);
},
);
flameTester.testGameWidget(
'plunger cannot excessively exceed starting position',
setUp: (game, tester) async {
await game.ensureAdd(plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(PrismaticJoint(jointDef));
plunger.body.setTransform(Vector2(0, -1), 0);
await tester.pump(const Duration(seconds: 1));
},
verify: (game, tester) async {
expect(plunger.body.position.y < 1, isTrue);
},
);
});
}

@ -1,14 +1,47 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../../../helpers/helpers.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
]);
}
Future<void> pump(
SpaceshipRamp children, {
required SpaceshipRampCubit bloc,
}) async {
await ensureAdd(
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>.value(
value: bloc,
children: [
ZCanvasComponent(children: [children]),
],
),
);
}
}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
@ -20,20 +53,8 @@ class _MockContact extends Mock implements Contact {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName, final flameTester = FlameTester(_TestGame.new);
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group( group(
'RampBallAscendingContactBehavior', 'RampBallAscendingContactBehavior',
@ -67,16 +88,18 @@ void main() {
initialState: const SpaceshipRampState.initial(), initialState: const SpaceshipRampState.initial(),
); );
final parent = SpaceshipRampBoardOpening.test(); final opening = SpaceshipRampBoardOpening.test();
final spaceshipRamp = SpaceshipRamp.test( final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc, children: [opening],
); );
when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
await spaceshipRamp.add(parent); await game.pump(
await game.ensureAddAll([spaceshipRamp, ball]); spaceshipRamp,
await parent.add(behavior); bloc: bloc,
);
await opening.ensureAdd(behavior);
behavior.beginContact(ball, _MockContact()); behavior.beginContact(ball, _MockContact());
@ -95,16 +118,18 @@ void main() {
initialState: const SpaceshipRampState.initial(), initialState: const SpaceshipRampState.initial(),
); );
final parent = SpaceshipRampBoardOpening.test(); final opening = SpaceshipRampBoardOpening.test();
final spaceshipRamp = SpaceshipRamp.test( final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc, children: [opening],
); );
when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
await spaceshipRamp.add(parent); await game.pump(
await game.ensureAddAll([spaceshipRamp, ball]); spaceshipRamp,
await parent.add(behavior); bloc: bloc,
);
await opening.ensureAdd(behavior);
behavior.beginContact(ball, _MockContact()); behavior.beginContact(ball, _MockContact());

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

Loading…
Cancel
Save