mirror of https://github.com/flutter/pinball.git
Merge branch 'feat/animation-sequences' of https://github.com/VGVentures/pinball into feat/animation-sequences
commit
d75bad5201
@ -1,44 +1,42 @@
|
||||
part of 'assets_manager_cubit.dart';
|
||||
|
||||
/// {@template assets_manager_state}
|
||||
/// State used to load the game assets
|
||||
/// State used to load the game assets.
|
||||
/// {@endtemplate}
|
||||
class AssetsManagerState extends Equatable {
|
||||
/// {@macro assets_manager_state}
|
||||
const AssetsManagerState({
|
||||
required this.loadables,
|
||||
required this.assetsCount,
|
||||
required this.loaded,
|
||||
});
|
||||
|
||||
/// {@macro assets_manager_state}
|
||||
const AssetsManagerState.initial()
|
||||
: this(loadables: const [], loaded: const []);
|
||||
const AssetsManagerState.initial() : this(assetsCount: 0, loaded: 0);
|
||||
|
||||
/// List of futures to load
|
||||
final List<Future> loadables;
|
||||
/// Number of assets to load.
|
||||
final int assetsCount;
|
||||
|
||||
/// List of loaded futures
|
||||
final List<Future> loaded;
|
||||
/// Number of already loaded assets.
|
||||
final int loaded;
|
||||
|
||||
/// Returns a value between 0 and 1 to indicate the loading progress
|
||||
double get progress =>
|
||||
loadables.isEmpty ? 0 : loaded.length / loadables.length;
|
||||
/// Returns a value between 0 and 1 to indicate the loading progress.
|
||||
double get progress => loaded == 0 ? 0 : loaded / assetsCount;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Returns a copy of this instance with the given parameters
|
||||
/// updated
|
||||
/// updated.
|
||||
AssetsManagerState copyWith({
|
||||
List<Future>? loadables,
|
||||
List<Future>? loaded,
|
||||
int? assetsCount,
|
||||
int? loaded,
|
||||
}) {
|
||||
return AssetsManagerState(
|
||||
loadables: loadables ?? this.loadables,
|
||||
assetsCount: assetsCount ?? this.assetsCount,
|
||||
loaded: loaded ?? this.loaded,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [loaded, loadables];
|
||||
List<Object> get props => [loaded, assetsCount];
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
export 'android_spaceship_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';
|
||||
|
@ -1,60 +1,40 @@
|
||||
import 'dart:async';
|
||||
|
||||
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_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template ramp_bonus_behavior}
|
||||
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
|
||||
/// {@endtemplate}
|
||||
class RampBonusBehavior extends Component with ParentIsA<SpaceshipRamp> {
|
||||
class RampBonusBehavior extends Component
|
||||
with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
|
||||
/// {@macro ramp_bonus_behavior}
|
||||
RampBonusBehavior({
|
||||
required Points points,
|
||||
}) : _points = points,
|
||||
super();
|
||||
|
||||
/// Creates a [RampBonusBehavior].
|
||||
///
|
||||
/// This can be used for testing [RampBonusBehavior] in isolation.
|
||||
@visibleForTesting
|
||||
RampBonusBehavior.test({
|
||||
required Points points,
|
||||
required this.subscription,
|
||||
}) : _points = points,
|
||||
super();
|
||||
|
||||
final Points _points;
|
||||
|
||||
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
|
||||
@visibleForTesting
|
||||
StreamSubscription? subscription;
|
||||
|
||||
@override
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
|
||||
subscription = subscription ??
|
||||
parent.bloc.stream.listen((state) {
|
||||
final achievedOneMillionPoints = state.hits % 10 == 0;
|
||||
|
||||
if (achievedOneMillionPoints) {
|
||||
parent.add(
|
||||
ScoringBehavior(
|
||||
points: _points,
|
||||
position: Vector2(0, -60),
|
||||
duration: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
bool listenWhen(
|
||||
SpaceshipRampState previousState,
|
||||
SpaceshipRampState newState,
|
||||
) {
|
||||
final hitsIncreased = previousState.hits < newState.hits;
|
||||
final achievedOneMillionPoints = newState.hits % 10 == 0;
|
||||
|
||||
return hitsIncreased && achievedOneMillionPoints;
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
subscription?.cancel();
|
||||
super.onRemove();
|
||||
void onNewState(SpaceshipRampState state) {
|
||||
parent!.add(
|
||||
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_bloc/flame_bloc.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:pinball/game/behaviors/behaviors.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template ramp_shot_behavior}
|
||||
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
|
||||
/// {@endtemplate}
|
||||
class RampShotBehavior extends Component
|
||||
with ParentIsA<SpaceshipRamp>, FlameBlocReader<GameBloc, GameState> {
|
||||
with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
|
||||
/// {@macro ramp_shot_behavior}
|
||||
RampShotBehavior({
|
||||
required Points points,
|
||||
}) : _points = points,
|
||||
super();
|
||||
|
||||
/// Creates a [RampShotBehavior].
|
||||
///
|
||||
/// This can be used for testing [RampShotBehavior] in isolation.
|
||||
@visibleForTesting
|
||||
RampShotBehavior.test({
|
||||
required Points points,
|
||||
required this.subscription,
|
||||
}) : _points = points,
|
||||
super();
|
||||
|
||||
final Points _points;
|
||||
|
||||
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
|
||||
@visibleForTesting
|
||||
StreamSubscription? subscription;
|
||||
|
||||
@override
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
|
||||
subscription = subscription ??
|
||||
parent.bloc.stream.listen((state) {
|
||||
final achievedOneMillionPoints = state.hits % 10 == 0;
|
||||
|
||||
if (!achievedOneMillionPoints) {
|
||||
bloc.add(const MultiplierIncreased());
|
||||
|
||||
parent.add(
|
||||
ScoringBehavior(
|
||||
points: _points,
|
||||
position: Vector2(0, -45),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
bool listenWhen(
|
||||
SpaceshipRampState previousState,
|
||||
SpaceshipRampState newState,
|
||||
) {
|
||||
return previousState.hits < newState.hits;
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
subscription?.cancel();
|
||||
super.onRemove();
|
||||
void onNewState(SpaceshipRampState state) {
|
||||
parent!.add(
|
||||
ScoringBehavior(
|
||||
points: _points,
|
||||
position: Vector2(0, -45),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,18 +1,15 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flame_bloc/flame_bloc.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
class AndroidSpaceshipEntranceBallContactBehavior
|
||||
extends ContactBehavior<AndroidSpaceshipEntrance>
|
||||
with FlameBlocReader<AndroidSpaceshipCubit, AndroidSpaceshipState> {
|
||||
class AndroidAnimatronicBallContactBehavior
|
||||
extends ContactBehavior<AndroidAnimatronic> {
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
bloc.onBallEntered();
|
||||
readBloc<AndroidSpaceshipCubit, AndroidSpaceshipState>().onBallContacted();
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'android_animatronic_ball_contact_behavior.dart.dart';
|
@ -1 +0,0 @@
|
||||
export 'android_spaceship_entrance_ball_contact_behavior.dart.dart';
|
@ -1,2 +1,3 @@
|
||||
export 'flipper_jointing_behavior.dart';
|
||||
export 'flipper_key_controlling_behavior.dart';
|
||||
export 'flipper_moving_behavior.dart';
|
||||
|
@ -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;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'google_word_animating_behavior.dart';
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,22 +1,55 @@
|
||||
// ignore_for_file: comment_references
|
||||
|
||||
part of 'spaceship_ramp_cubit.dart';
|
||||
|
||||
class SpaceshipRampState extends Equatable {
|
||||
const SpaceshipRampState({
|
||||
required this.hits,
|
||||
required this.lightState,
|
||||
}) : 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 ArrowLightState lightState;
|
||||
|
||||
bool get arrowFullyLit => lightState == ArrowLightState.active5;
|
||||
|
||||
SpaceshipRampState copyWith({
|
||||
int? hits,
|
||||
ArrowLightState? lightState,
|
||||
}) {
|
||||
return SpaceshipRampState(
|
||||
hits: hits ?? this.hits,
|
||||
lightState: lightState ?? this.lightState,
|
||||
);
|
||||
}
|
||||
|
||||
@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,
|
||||
}
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue