@ -1,11 +1,33 @@
|
||||
{
|
||||
"firestore": {
|
||||
"rules": "firestore.rules"
|
||||
},
|
||||
"hosting": {
|
||||
"public": "build/web",
|
||||
"site": "ashehwkdkdjruejdnensjsjdne",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "**/*.@(jpg|jpeg|gif|png)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "max-age=3600"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache, no-store, must-revalidate"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"storage": {
|
||||
"rules": "storage.rules"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
rules_version = '2';
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
match /leaderboard/{userId} {
|
||||
|
||||
function prohibited(initials) {
|
||||
let prohibitedInitials = get(/databases/$(database)/documents/prohibitedInitials/list).data.prohibitedInitials;
|
||||
return initials in prohibitedInitials;
|
||||
}
|
||||
|
||||
function inCharLimit(initials) {
|
||||
return initials.size() < 4;
|
||||
}
|
||||
|
||||
function isAuthedUser(auth) {
|
||||
return request.auth.uid != null; && auth.token.firebase.sign_in_provider == "anonymous"
|
||||
}
|
||||
|
||||
// Leaderboard can be read if it doesn't contain any prohibited initials
|
||||
allow read: if !prohibited(resource.data.playerInitials);
|
||||
|
||||
// A leaderboard entry can be created if the user is authenticated,
|
||||
// it's 3 characters long, and not a prohibited combination.
|
||||
allow create: if isAuthedUser(request.auth) &&
|
||||
inCharLimit(request.resource.data.playerInitials) &&
|
||||
!prohibited(request.resource.data.playerInitials);
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1,3 @@
|
||||
export 'android_spaceship_bonus_behavior.dart';
|
||||
export 'ramp_bonus_behavior.dart';
|
||||
export 'ramp_shot_behavior.dart';
|
||||
|
@ -0,0 +1,62 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.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_bonus_behavior}
|
||||
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
|
||||
/// {@endtemplate}
|
||||
class RampBonusBehavior extends Component
|
||||
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
|
||||
/// {@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,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
subscription?.cancel();
|
||||
super.onRemove();
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.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>, HasGameRef<PinballGame> {
|
||||
/// {@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) {
|
||||
gameRef.read<GameBloc>().add(const MultiplierIncreased());
|
||||
|
||||
parent.add(
|
||||
ScoringBehavior(
|
||||
points: _points,
|
||||
position: Vector2(0, -45),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
subscription?.cancel();
|
||||
super.onRemove();
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export 'bloc/start_game_bloc.dart';
|
||||
export 'widgets/start_game_listener.dart';
|
||||
|
@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/how_to_play/how_to_play.dart';
|
||||
import 'package:pinball/select_character/select_character.dart';
|
||||
import 'package:pinball/start_game/start_game.dart';
|
||||
import 'package:pinball_audio/pinball_audio.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// {@template start_game_listener}
|
||||
/// Listener that manages the display of dialogs for [StartGameStatus].
|
||||
///
|
||||
/// It's responsible for starting the game after pressing play button
|
||||
/// and playing a sound after the 'how to play' dialog.
|
||||
/// {@endtemplate}
|
||||
class StartGameListener extends StatelessWidget {
|
||||
/// {@macro start_game_listener}
|
||||
const StartGameListener({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required PinballGame game,
|
||||
}) : _child = child,
|
||||
_game = game,
|
||||
super(key: key);
|
||||
|
||||
final Widget _child;
|
||||
final PinballGame _game;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<StartGameBloc, StartGameState>(
|
||||
listener: (context, state) {
|
||||
switch (state.status) {
|
||||
case StartGameStatus.initial:
|
||||
break;
|
||||
case StartGameStatus.selectCharacter:
|
||||
_onSelectCharacter(context);
|
||||
_game.gameFlowController.start();
|
||||
break;
|
||||
case StartGameStatus.howToPlay:
|
||||
_onHowToPlay(context);
|
||||
break;
|
||||
case StartGameStatus.play:
|
||||
break;
|
||||
}
|
||||
},
|
||||
child: _child,
|
||||
);
|
||||
}
|
||||
|
||||
void _onSelectCharacter(BuildContext context) {
|
||||
_showPinballDialog(
|
||||
context: context,
|
||||
child: const CharacterSelectionDialog(),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _onHowToPlay(BuildContext context) {
|
||||
final audio = context.read<PinballAudio>();
|
||||
|
||||
_showPinballDialog(
|
||||
context: context,
|
||||
child: HowToPlayDialog(
|
||||
onDismissCallback: () {
|
||||
context.read<StartGameBloc>().add(const HowToPlayFinished());
|
||||
audio.ioPinballVoiceOver();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPinballDialog({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
bool barrierDismissible = true,
|
||||
}) {
|
||||
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierColor: PinballColors.transparent,
|
||||
barrierDismissible: barrierDismissible,
|
||||
builder: (_) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: gameWidgetWidth * 0.87,
|
||||
width: gameWidgetWidth,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1 @@
|
||||
export 'ramp_ball_ascending_contact_behavior.dart';
|
@ -0,0 +1,24 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template ramp_ball_ascending_contact_behavior}
|
||||
/// Detects an ascending [Ball] that enters into the [SpaceshipRamp].
|
||||
///
|
||||
/// The [Ball] can hit with sensor to recognize if a [Ball] goes into or out of
|
||||
/// the [SpaceshipRamp].
|
||||
/// {@endtemplate}
|
||||
class RampBallAscendingContactBehavior
|
||||
extends ContactBehavior<RampScoringSensor> {
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
if (other.body.linearVelocity.y < 0) {
|
||||
parent.parent.bloc.onAscendingBallEntered();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'spaceship_ramp_state.dart';
|
||||
|
||||
class SpaceshipRampCubit extends Cubit<SpaceshipRampState> {
|
||||
SpaceshipRampCubit() : super(const SpaceshipRampState.initial());
|
||||
|
||||
void onAscendingBallEntered() {
|
||||
emit(
|
||||
state.copyWith(hits: state.hits + 1),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
part of 'spaceship_ramp_cubit.dart';
|
||||
|
||||
class SpaceshipRampState extends Equatable {
|
||||
const SpaceshipRampState({
|
||||
required this.hits,
|
||||
}) : assert(hits >= 0, "Hits can't be negative");
|
||||
|
||||
const SpaceshipRampState.initial() : this(hits: 0);
|
||||
|
||||
final int hits;
|
||||
|
||||
SpaceshipRampState copyWith({
|
||||
int? hits,
|
||||
}) {
|
||||
return SpaceshipRampState(
|
||||
hits: hits ?? this.hits,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [hits];
|
||||
}
|
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 27 KiB |
@ -0,0 +1,117 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
|
||||
|
||||
import '../../../../helpers/helpers.dart';
|
||||
|
||||
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
|
||||
|
||||
class _MockBall extends Mock implements Ball {}
|
||||
|
||||
class _MockBody extends Mock implements Body {}
|
||||
|
||||
class _MockContact extends Mock implements Contact {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final assets = [
|
||||
Assets.images.android.ramp.boardOpening.keyName,
|
||||
Assets.images.android.ramp.railingForeground.keyName,
|
||||
Assets.images.android.ramp.railingBackground.keyName,
|
||||
Assets.images.android.ramp.main.keyName,
|
||||
Assets.images.android.ramp.arrow.inactive.keyName,
|
||||
Assets.images.android.ramp.arrow.active1.keyName,
|
||||
Assets.images.android.ramp.arrow.active2.keyName,
|
||||
Assets.images.android.ramp.arrow.active3.keyName,
|
||||
Assets.images.android.ramp.arrow.active4.keyName,
|
||||
Assets.images.android.ramp.arrow.active5.keyName,
|
||||
];
|
||||
|
||||
final flameTester = FlameTester(() => TestGame(assets));
|
||||
|
||||
group(
|
||||
'RampBallAscendingContactBehavior',
|
||||
() {
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
RampBallAscendingContactBehavior(),
|
||||
isA<RampBallAscendingContactBehavior>(),
|
||||
);
|
||||
});
|
||||
|
||||
group('beginContact', () {
|
||||
late Ball ball;
|
||||
late Body body;
|
||||
|
||||
setUp(() {
|
||||
ball = _MockBall();
|
||||
body = _MockBody();
|
||||
|
||||
when(() => ball.body).thenReturn(body);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
"calls 'onAscendingBallEntered' when a ball enters into the ramp",
|
||||
(game) async {
|
||||
final behavior = RampBallAscendingContactBehavior();
|
||||
final bloc = _MockSpaceshipRampCubit();
|
||||
whenListen(
|
||||
bloc,
|
||||
const Stream<SpaceshipRampState>.empty(),
|
||||
initialState: const SpaceshipRampState.initial(),
|
||||
);
|
||||
|
||||
final rampSensor = RampScoringSensor.test();
|
||||
final spaceshipRamp = SpaceshipRamp.test(
|
||||
bloc: bloc,
|
||||
);
|
||||
|
||||
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
|
||||
|
||||
await spaceshipRamp.add(rampSensor);
|
||||
await game.ensureAddAll([spaceshipRamp, ball]);
|
||||
await rampSensor.add(behavior);
|
||||
|
||||
behavior.beginContact(ball, _MockContact());
|
||||
|
||||
verify(bloc.onAscendingBallEntered).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
"doesn't call 'onAscendingBallEntered' when a ball goes out the ramp",
|
||||
(game) async {
|
||||
final behavior = RampBallAscendingContactBehavior();
|
||||
final bloc = _MockSpaceshipRampCubit();
|
||||
whenListen(
|
||||
bloc,
|
||||
const Stream<SpaceshipRampState>.empty(),
|
||||
initialState: const SpaceshipRampState.initial(),
|
||||
);
|
||||
|
||||
final rampSensor = RampScoringSensor.test();
|
||||
final spaceshipRamp = SpaceshipRamp.test(
|
||||
bloc: bloc,
|
||||
);
|
||||
|
||||
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
|
||||
|
||||
await spaceshipRamp.add(rampSensor);
|
||||
await game.ensureAddAll([spaceshipRamp, ball]);
|
||||
await rampSensor.add(behavior);
|
||||
|
||||
behavior.beginContact(ball, _MockContact());
|
||||
|
||||
verifyNever(bloc.onAscendingBallEntered);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
void main() {
|
||||
group('SpaceshipRampCubit', () {
|
||||
group('onAscendingBallEntered', () {
|
||||
blocTest<SpaceshipRampCubit, SpaceshipRampState>(
|
||||
'emits hits incremented and arrow goes to the next value',
|
||||
build: SpaceshipRampCubit.new,
|
||||
act: (bloc) => bloc
|
||||
..onAscendingBallEntered()
|
||||
..onAscendingBallEntered()
|
||||
..onAscendingBallEntered(),
|
||||
expect: () => [
|
||||
SpaceshipRampState(hits: 1),
|
||||
SpaceshipRampState(hits: 2),
|
||||
SpaceshipRampState(hits: 3),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/src/components/components.dart';
|
||||
|
||||
void main() {
|
||||
group('SpaceshipRampState', () {
|
||||
test('supports value equality', () {
|
||||
expect(
|
||||
SpaceshipRampState(hits: 0),
|
||||
equals(
|
||||
SpaceshipRampState(hits: 0),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
group('constructor', () {
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
SpaceshipRampState(hits: 0),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'throws AssertionError '
|
||||
'when hits is negative',
|
||||
() {
|
||||
expect(
|
||||
() => SpaceshipRampState(hits: -1),
|
||||
throwsAssertionError,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('copyWith', () {
|
||||
test(
|
||||
'throws AssertionError '
|
||||
'when hits is decreased',
|
||||
() {
|
||||
const rampState = SpaceshipRampState(hits: 0);
|
||||
expect(
|
||||
() => rampState.copyWith(hits: rampState.hits - 1),
|
||||
throwsAssertionError,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'copies correctly '
|
||||
'when no argument specified',
|
||||
() {
|
||||
const rampState = SpaceshipRampState(hits: 0);
|
||||
expect(
|
||||
rampState.copyWith(),
|
||||
equals(rampState),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'copies correctly '
|
||||
'when all arguments specified',
|
||||
() {
|
||||
const rampState = SpaceshipRampState(hits: 0);
|
||||
final otherRampState = SpaceshipRampState(hits: rampState.hits + 1);
|
||||
expect(rampState, isNot(equals(otherRampState)));
|
||||
|
||||
expect(
|
||||
rampState.copyWith(hits: rampState.hits + 1),
|
||||
equals(otherRampState),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 6.3 KiB |
@ -0,0 +1,152 @@
|
||||
// ignore_for_file: cascade_invocations, prefer_const_constructors
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball/game/behaviors/behaviors.dart';
|
||||
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
import '../../../../helpers/helpers.dart';
|
||||
|
||||
class _MockGameBloc extends Mock implements GameBloc {}
|
||||
|
||||
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
|
||||
|
||||
class _MockStreamSubscription extends Mock
|
||||
implements StreamSubscription<SpaceshipRampState> {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final assets = [
|
||||
Assets.images.android.ramp.boardOpening.keyName,
|
||||
Assets.images.android.ramp.railingForeground.keyName,
|
||||
Assets.images.android.ramp.railingBackground.keyName,
|
||||
Assets.images.android.ramp.main.keyName,
|
||||
Assets.images.android.ramp.arrow.inactive.keyName,
|
||||
Assets.images.android.ramp.arrow.active1.keyName,
|
||||
Assets.images.android.ramp.arrow.active2.keyName,
|
||||
Assets.images.android.ramp.arrow.active3.keyName,
|
||||
Assets.images.android.ramp.arrow.active4.keyName,
|
||||
Assets.images.android.ramp.arrow.active5.keyName,
|
||||
Assets.images.android.rail.main.keyName,
|
||||
Assets.images.android.rail.exit.keyName,
|
||||
Assets.images.score.oneMillion.keyName,
|
||||
];
|
||||
|
||||
group('RampBonusBehavior', () {
|
||||
const shotPoints = Points.oneMillion;
|
||||
|
||||
late GameBloc gameBloc;
|
||||
|
||||
setUp(() {
|
||||
gameBloc = _MockGameBloc();
|
||||
whenListen(
|
||||
gameBloc,
|
||||
const Stream<GameState>.empty(),
|
||||
initialState: const GameState.initial(),
|
||||
);
|
||||
});
|
||||
|
||||
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
|
||||
gameBuilder: EmptyPinballTestGame.new,
|
||||
blocBuilder: () => gameBloc,
|
||||
assets: assets,
|
||||
);
|
||||
|
||||
flameBlocTester.testGameWidget(
|
||||
'when hits are multiples of 10 times adds a ScoringBehavior',
|
||||
setUp: (game, tester) async {
|
||||
final bloc = _MockSpaceshipRampCubit();
|
||||
final streamController = StreamController<SpaceshipRampState>();
|
||||
whenListen(
|
||||
bloc,
|
||||
streamController.stream,
|
||||
initialState: SpaceshipRampState(hits: 9),
|
||||
);
|
||||
final behavior = RampBonusBehavior(
|
||||
points: shotPoints,
|
||||
);
|
||||
final parent = SpaceshipRamp.test(
|
||||
bloc: bloc,
|
||||
);
|
||||
|
||||
await game.ensureAdd(ZCanvasComponent(children: [parent]));
|
||||
await parent.ensureAdd(behavior);
|
||||
|
||||
streamController.add(SpaceshipRampState(hits: 10));
|
||||
|
||||
final scores = game.descendants().whereType<ScoringBehavior>();
|
||||
await game.ready();
|
||||
|
||||
expect(scores.length, 1);
|
||||
},
|
||||
);
|
||||
|
||||
flameBlocTester.testGameWidget(
|
||||
"when hits are not multiple of 10 times doesn't add any ScoringBehavior",
|
||||
setUp: (game, tester) async {
|
||||
final bloc = _MockSpaceshipRampCubit();
|
||||
final streamController = StreamController<SpaceshipRampState>();
|
||||
whenListen(
|
||||
bloc,
|
||||
streamController.stream,
|
||||
initialState: SpaceshipRampState.initial(),
|
||||
);
|
||||
final behavior = RampBonusBehavior(
|
||||
points: shotPoints,
|
||||
);
|
||||
final parent = SpaceshipRamp.test(
|
||||
bloc: bloc,
|
||||
);
|
||||
|
||||
await game.ensureAdd(ZCanvasComponent(children: [parent]));
|
||||
await parent.ensureAdd(behavior);
|
||||
|
||||
streamController.add(SpaceshipRampState(hits: 1));
|
||||
|
||||
final scores = game.descendants().whereType<ScoringBehavior>();
|
||||
await game.ready();
|
||||
|
||||
expect(scores.length, 0);
|
||||
},
|
||||
);
|
||||
|
||||
flameBlocTester.testGameWidget(
|
||||
'closes subscription when removed',
|
||||
setUp: (game, tester) async {
|
||||
final bloc = _MockSpaceshipRampCubit();
|
||||
whenListen(
|
||||
bloc,
|
||||
const Stream<SpaceshipRampState>.empty(),
|
||||
initialState: SpaceshipRampState.initial(),
|
||||
);
|
||||
when(bloc.close).thenAnswer((_) async {});
|
||||
|
||||
final subscription = _MockStreamSubscription();
|
||||
when(subscription.cancel).thenAnswer((_) async {});
|
||||
|
||||
final behavior = RampBonusBehavior.test(
|
||||
points: shotPoints,
|
||||
subscription: subscription,
|
||||
);
|
||||
final parent = SpaceshipRamp.test(
|
||||
bloc: bloc,
|
||||
);
|
||||
|
||||
await game.ensureAdd(ZCanvasComponent(children: [parent]));
|
||||
await parent.ensureAdd(behavior);
|
||||
|
||||
parent.remove(behavior);
|
||||
await game.ready();
|
||||
|
||||
verify(subscription.cancel).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
// ignore_for_file: cascade_invocations, prefer_const_constructors
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball/game/behaviors/behaviors.dart';
|
||||
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
import '../../../../helpers/helpers.dart';
|
||||
|
||||
class _MockGameBloc extends Mock implements GameBloc {}
|
||||
|
||||
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
|
||||
|
||||
class _MockStreamSubscription extends Mock
|
||||
implements StreamSubscription<SpaceshipRampState> {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final assets = [
|
||||
Assets.images.android.ramp.boardOpening.keyName,
|
||||
Assets.images.android.ramp.railingForeground.keyName,
|
||||
Assets.images.android.ramp.railingBackground.keyName,
|
||||
Assets.images.android.ramp.main.keyName,
|
||||
Assets.images.android.ramp.arrow.inactive.keyName,
|
||||
Assets.images.android.ramp.arrow.active1.keyName,
|
||||
Assets.images.android.ramp.arrow.active2.keyName,
|
||||
Assets.images.android.ramp.arrow.active3.keyName,
|
||||
Assets.images.android.ramp.arrow.active4.keyName,
|
||||
Assets.images.android.ramp.arrow.active5.keyName,
|
||||
Assets.images.android.rail.main.keyName,
|
||||
Assets.images.android.rail.exit.keyName,
|
||||
Assets.images.score.fiveThousand.keyName,
|
||||
];
|
||||
|
||||
group('RampShotBehavior', () {
|
||||
const shotPoints = Points.fiveThousand;
|
||||
|
||||
late GameBloc gameBloc;
|
||||
|
||||
setUp(() {
|
||||
gameBloc = _MockGameBloc();
|
||||
whenListen(
|
||||
gameBloc,
|
||||
const Stream<GameState>.empty(),
|
||||
initialState: const GameState.initial(),
|
||||
);
|
||||
});
|
||||
|
||||
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
|
||||
gameBuilder: EmptyPinballTestGame.new,
|
||||
blocBuilder: () => gameBloc,
|
||||
assets: assets,
|
||||
);
|
||||
|
||||
flameBlocTester.testGameWidget(
|
||||
'when hits are not multiple of 10 times '
|
||||
'increases multiplier and adds a ScoringBehavior',
|
||||
setUp: (game, tester) async {
|
||||
final bloc = _MockSpaceshipRampCubit();
|
||||
final streamController = StreamController<SpaceshipRampState>();
|
||||
whenListen(
|
||||
bloc,
|
||||
streamController.stream,
|
||||
initialState: SpaceshipRampState.initial(),
|
||||
);
|
||||
final behavior = RampShotBehavior(
|
||||
points: shotPoints,
|
||||
);
|
||||
final parent = SpaceshipRamp.test(
|
||||
bloc: bloc,
|
||||
);
|
||||
|
||||
await game.ensureAdd(ZCanvasComponent(children: [parent]));
|
||||
await parent.ensureAdd(behavior);
|
||||
|
||||
streamController.add(SpaceshipRampState(hits: 1));
|
||||
|
||||
final scores = game.descendants().whereType<ScoringBehavior>();
|
||||
await game.ready();
|
||||
|
||||
verify(() => gameBloc.add(MultiplierIncreased())).called(1);
|
||||
expect(scores.length, 1);
|
||||
},
|
||||
);
|
||||
|
||||
flameBlocTester.testGameWidget(
|
||||
'when hits multiple of 10 times '
|
||||
"doesn't increase multiplier, neither ScoringBehavior",
|
||||
setUp: (game, tester) async {
|
||||
final bloc = _MockSpaceshipRampCubit();
|
||||
final streamController = StreamController<SpaceshipRampState>();
|
||||
whenListen(
|
||||
bloc,
|
||||
streamController.stream,
|
||||
initialState: SpaceshipRampState(hits: 9),
|
||||
);
|
||||
final behavior = RampShotBehavior(
|
||||
points: shotPoints,
|
||||
);
|
||||
final parent = SpaceshipRamp.test(
|
||||
bloc: bloc,
|
||||
);
|
||||
|
||||
await game.ensureAdd(ZCanvasComponent(children: [parent]));
|
||||
await parent.ensureAdd(behavior);
|
||||
|
||||
streamController.add(SpaceshipRampState(hits: 10));
|
||||
|
||||
final scores = game.children.whereType<ScoringBehavior>();
|
||||
await game.ready();
|
||||
|
||||
verifyNever(() => gameBloc.add(MultiplierIncreased()));
|
||||
expect(scores.length, 0);
|
||||
},
|
||||
);
|
||||
|
||||
flameBlocTester.testGameWidget(
|
||||
'closes subscription when removed',
|
||||
setUp: (game, tester) async {
|
||||
final bloc = _MockSpaceshipRampCubit();
|
||||
whenListen(
|
||||
bloc,
|
||||
const Stream<SpaceshipRampState>.empty(),
|
||||
initialState: SpaceshipRampState.initial(),
|
||||
);
|
||||
when(bloc.close).thenAnswer((_) async {});
|
||||
|
||||
final subscription = _MockStreamSubscription();
|
||||
when(subscription.cancel).thenAnswer((_) async {});
|
||||
|
||||
final behavior = RampShotBehavior.test(
|
||||
points: shotPoints,
|
||||
subscription: subscription,
|
||||
);
|
||||
final parent = SpaceshipRamp.test(
|
||||
bloc: bloc,
|
||||
);
|
||||
|
||||
await game.ensureAdd(ZCanvasComponent(children: [parent]));
|
||||
await parent.ensureAdd(behavior);
|
||||
|
||||
parent.remove(behavior);
|
||||
await game.ready();
|
||||
|
||||
verify(subscription.cancel).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,269 @@
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/how_to_play/how_to_play.dart';
|
||||
import 'package:pinball/select_character/select_character.dart';
|
||||
import 'package:pinball/start_game/start_game.dart';
|
||||
import 'package:pinball_audio/pinball_audio.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
class _MockStartGameBloc extends Mock implements StartGameBloc {}
|
||||
|
||||
class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {}
|
||||
|
||||
class _MockPinballGame extends Mock implements PinballGame {}
|
||||
|
||||
class _MockGameFlowController extends Mock implements GameFlowController {}
|
||||
|
||||
class _MockPinballAudio extends Mock implements PinballAudio {}
|
||||
|
||||
void main() {
|
||||
late StartGameBloc startGameBloc;
|
||||
late PinballGame pinballGame;
|
||||
late PinballAudio pinballAudio;
|
||||
late CharacterThemeCubit characterThemeCubit;
|
||||
|
||||
group('StartGameListener', () {
|
||||
setUp(() async {
|
||||
await mockFlameImages();
|
||||
|
||||
startGameBloc = _MockStartGameBloc();
|
||||
pinballGame = _MockPinballGame();
|
||||
pinballAudio = _MockPinballAudio();
|
||||
characterThemeCubit = _MockCharacterThemeCubit();
|
||||
});
|
||||
|
||||
group('on selectCharacter status', () {
|
||||
testWidgets(
|
||||
'calls start on the game controller',
|
||||
(tester) async {
|
||||
whenListen(
|
||||
startGameBloc,
|
||||
Stream.value(
|
||||
const StartGameState(status: StartGameStatus.selectCharacter),
|
||||
),
|
||||
initialState: const StartGameState.initial(),
|
||||
);
|
||||
final gameController = _MockGameFlowController();
|
||||
when(() => pinballGame.gameFlowController)
|
||||
.thenAnswer((_) => gameController);
|
||||
|
||||
await tester.pumpApp(
|
||||
StartGameListener(
|
||||
game: pinballGame,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
startGameBloc: startGameBloc,
|
||||
);
|
||||
|
||||
verify(gameController.start).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'shows SelectCharacter dialog',
|
||||
(tester) async {
|
||||
whenListen(
|
||||
startGameBloc,
|
||||
Stream.value(
|
||||
const StartGameState(status: StartGameStatus.selectCharacter),
|
||||
),
|
||||
initialState: const StartGameState.initial(),
|
||||
);
|
||||
whenListen(
|
||||
characterThemeCubit,
|
||||
Stream.value(const CharacterThemeState.initial()),
|
||||
initialState: const CharacterThemeState.initial(),
|
||||
);
|
||||
final gameController = _MockGameFlowController();
|
||||
when(() => pinballGame.gameFlowController)
|
||||
.thenAnswer((_) => gameController);
|
||||
|
||||
await tester.pumpApp(
|
||||
StartGameListener(
|
||||
game: pinballGame,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
startGameBloc: startGameBloc,
|
||||
characterThemeCubit: characterThemeCubit,
|
||||
);
|
||||
|
||||
await tester.pump(kThemeAnimationDuration);
|
||||
|
||||
expect(
|
||||
find.byType(CharacterSelectionDialog),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'on howToPlay status shows HowToPlay dialog',
|
||||
(tester) async {
|
||||
whenListen(
|
||||
startGameBloc,
|
||||
Stream.value(
|
||||
const StartGameState(status: StartGameStatus.howToPlay),
|
||||
),
|
||||
initialState: const StartGameState.initial(),
|
||||
);
|
||||
|
||||
await tester.pumpApp(
|
||||
StartGameListener(
|
||||
game: pinballGame,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
startGameBloc: startGameBloc,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(HowToPlayDialog),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'do nothing on play status',
|
||||
(tester) async {
|
||||
whenListen(
|
||||
startGameBloc,
|
||||
Stream.value(
|
||||
const StartGameState(status: StartGameStatus.play),
|
||||
),
|
||||
initialState: const StartGameState.initial(),
|
||||
);
|
||||
|
||||
await tester.pumpApp(
|
||||
StartGameListener(
|
||||
game: pinballGame,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
startGameBloc: startGameBloc,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(HowToPlayDialog),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byType(CharacterSelectionDialog),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'do nothing on initial status',
|
||||
(tester) async {
|
||||
whenListen(
|
||||
startGameBloc,
|
||||
Stream.value(
|
||||
const StartGameState(status: StartGameStatus.initial),
|
||||
),
|
||||
initialState: const StartGameState.initial(),
|
||||
);
|
||||
|
||||
await tester.pumpApp(
|
||||
StartGameListener(
|
||||
game: pinballGame,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
startGameBloc: startGameBloc,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(HowToPlayDialog),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byType(CharacterSelectionDialog),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('on dismiss HowToPlayDialog', () {
|
||||
setUp(() {
|
||||
whenListen(
|
||||
startGameBloc,
|
||||
Stream.value(
|
||||
const StartGameState(status: StartGameStatus.howToPlay),
|
||||
),
|
||||
initialState: const StartGameState.initial(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'adds HowToPlayFinished event',
|
||||
(tester) async {
|
||||
await tester.pumpApp(
|
||||
StartGameListener(
|
||||
game: pinballGame,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
startGameBloc: startGameBloc,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(HowToPlayDialog),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tapAt(const Offset(1, 1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(HowToPlayDialog),
|
||||
findsNothing,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(
|
||||
() => startGameBloc.add(const HowToPlayFinished()),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'plays the I/O Pinball voice over audio',
|
||||
(tester) async {
|
||||
await tester.pumpApp(
|
||||
StartGameListener(
|
||||
game: pinballGame,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
startGameBloc: startGameBloc,
|
||||
pinballAudio: pinballAudio,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(HowToPlayDialog),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tapAt(const Offset(1, 1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(HowToPlayDialog),
|
||||
findsNothing,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(pinballAudio.ioPinballVoiceOver).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|