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';
|
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];
|
||||||
}
|
}
|
||||||
|
@ -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 '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';
|
||||||
|
@ -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(
|
@override
|
||||||
|
void onNewState(SpaceshipRampState state) {
|
||||||
|
parent!.add(
|
||||||
ScoringBehavior(
|
ScoringBehavior(
|
||||||
points: _points,
|
points: _points,
|
||||||
position: Vector2(0, -45),
|
position: Vector2(0, -45),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onRemove() {
|
|
||||||
subscription?.cancel();
|
|
||||||
super.onRemove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
// 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';
|
@ -1 +0,0 @@
|
|||||||
export 'android_spaceship_entrance_ball_contact_behavior.dart.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';
|
||||||
|
@ -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';
|
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,
|
||||||
}
|
}
|
||||||
|
@ -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