Merge branch 'main' into feat/spaceship-ramp-shot-logic

pull/296/head
RuiAlonso 3 years ago
commit 90ec9de27d

@ -10,6 +10,7 @@ export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart';
export 'google_word/google_word.dart';
export 'launcher.dart';
export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart';
export 'scoring_behavior.dart';
export 'sparky_scorch.dart';

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

@ -0,0 +1,28 @@
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';
/// Toggle each [Multiball] when there is a bonus ball.
class MultiballsBehavior extends Component
with
HasGameRef<PinballGame>,
ParentIsA<Multiballs>,
BlocComponent<GameBloc, GameState> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
final hasChanged = previousState?.bonusHistory != newState.bonusHistory;
final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty &&
newState.bonusHistory.last == GameBonus.dashNest;
return hasChanged && lastBonusIsMultiball;
}
@override
void onNewState(GameState state) {
parent.children.whereType<Multiball>().forEach((multiball) {
multiball.bloc.onAnimate();
});
}
}

@ -0,0 +1,30 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template multiballs_component}
/// A [SpriteGroupComponent] for the multiball over the board.
/// {@endtemplate}
class Multiballs extends Component with ZIndex {
/// {@macro multiballs_component}
Multiballs()
: super(
children: [
Multiball.a(),
Multiball.b(),
Multiball.c(),
Multiball.d(),
MultiballsBehavior(),
],
) {
zIndex = ZIndexes.decal;
}
/// Creates a [Multiballs] without any children.
///
/// This can be used for testing [Multiballs]'s behaviors in isolation.
@visibleForTesting
Multiballs.test();
}

@ -114,6 +114,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiball.lit.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName),
images.load(components.Assets.images.multiplier.x2.dimmed.keyName),
images.load(components.Assets.images.multiplier.x3.lit.keyName),

@ -53,6 +53,7 @@ class PinballGame extends Forge2DGame
final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(),
Multiballs(),
];
final characterAreas = [
AndroidAcres(),
@ -87,7 +88,7 @@ class PinballGame extends Forge2DGame
// NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually.
if (bounds.contains(info.eventPosition.game.toOffset())) {
descendants().whereType<Plunger>().single.pull();
descendants().whereType<Plunger>().single.pullFor(2);
} else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right;
@ -103,21 +104,12 @@ class PinballGame extends Forge2DGame
@override
void onTapUp(TapUpInfo info) {
final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size;
if (bounds.contains(info.eventPosition.game.toOffset())) {
descendants().whereType<Plunger>().single.release();
} else {
_moveFlippersDown();
}
_moveFlippersDown();
super.onTapUp(info);
}
@override
void onTapCancel() {
descendants().whereType<Plunger>().single.release();
_moveFlippersDown();
super.onTapCancel();
}

@ -52,7 +52,6 @@ extension on Control {
Future<void> showHowToPlayDialog(BuildContext context) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => HowToPlayDialog(),
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -28,6 +28,7 @@ class $AssetsImagesGen {
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen();
$AssetsImagesMultiballGen get multiball => const $AssetsImagesMultiballGen();
$AssetsImagesMultiplierGen get multiplier =>
const $AssetsImagesMultiplierGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
@ -180,6 +181,18 @@ class $AssetsImagesLaunchRampGen {
const AssetGenImage('assets/images/launch_ramp/ramp.png');
}
class $AssetsImagesMultiballGen {
const $AssetsImagesMultiballGen();
/// File path: assets/images/multiball/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiball/dimmed.png');
/// File path: assets/images/multiball/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiball/lit.png');
}
class $AssetsImagesMultiplierGen {
const $AssetsImagesMultiplierGen();

@ -21,6 +21,7 @@ export 'kicker/kicker.dart';
export 'launch_ramp.dart';
export 'layer.dart';
export 'layer_sensor.dart';
export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart';
export 'rocket.dart';

@ -0,0 +1,78 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template multiball_blinking_behavior}
/// Makes a [Multiball] blink back to [MultiballLightState.lit] when
/// [MultiballLightState.dimmed].
/// {@endtemplate}
class MultiballBlinkingBehavior extends TimerComponent
with ParentIsA<Multiball> {
/// {@macro multiball_blinking_behavior}
MultiballBlinkingBehavior() : super(period: 0.1);
final _maxBlinks = 10;
int _blinksCounter = 0;
bool _isAnimating = false;
void _onNewState(MultiballState state) {
final animationEnabled =
state.animationState == MultiballAnimationState.blinking;
final canBlink = _blinksCounter < _maxBlinks;
if (animationEnabled && canBlink) {
_start();
} else {
_stop();
}
}
void _start() {
if (!_isAnimating) {
_isAnimating = true;
timer
..reset()
..start();
_animate();
}
}
void _animate() {
parent.bloc.onBlink();
_blinksCounter++;
}
void _stop() {
if (_isAnimating) {
_isAnimating = false;
timer.stop();
_blinksCounter = 0;
parent.bloc.onStop();
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
if (!_isAnimating) {
timer.stop();
} else {
if (_blinksCounter < _maxBlinks) {
_animate();
timer
..reset()
..start();
} else {
timer.stop();
}
}
}
}

@ -0,0 +1,37 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'multiball_state.dart';
class MultiballCubit extends Cubit<MultiballState> {
MultiballCubit() : super(const MultiballState.initial());
void onAnimate() {
emit(
state.copyWith(animationState: MultiballAnimationState.blinking),
);
}
void onStop() {
emit(
state.copyWith(animationState: MultiballAnimationState.idle),
);
}
void onBlink() {
switch (state.lightState) {
case MultiballLightState.lit:
emit(
state.copyWith(lightState: MultiballLightState.dimmed),
);
break;
case MultiballLightState.dimmed:
emit(
state.copyWith(lightState: MultiballLightState.lit),
);
break;
}
}
}

@ -0,0 +1,44 @@
// ignore_for_file: comment_references, public_member_api_docs
part of 'multiball_cubit.dart';
/// Indicates the different sprite states for [MultiballSpriteGroupComponent].
enum MultiballLightState {
lit,
dimmed,
}
// Indicates if the blinking animation is running.
enum MultiballAnimationState {
idle,
blinking,
}
class MultiballState extends Equatable {
const MultiballState({
required this.lightState,
required this.animationState,
});
const MultiballState.initial()
: this(
lightState: MultiballLightState.dimmed,
animationState: MultiballAnimationState.idle,
);
final MultiballLightState lightState;
final MultiballAnimationState animationState;
MultiballState copyWith({
MultiballLightState? lightState,
MultiballAnimationState? animationState,
}) {
return MultiballState(
lightState: lightState ?? this.lightState,
animationState: animationState ?? this.animationState,
);
}
@override
List<Object> get props => [lightState, animationState];
}

@ -0,0 +1,138 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import 'package:pinball_components/src/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/multiball_cubit.dart';
/// {@template multiball}
/// A [Component] for the multiball lighting decals on the board.
/// {@endtemplate}
class Multiball extends Component {
/// {@macro multiball}
Multiball._({
required Vector2 position,
double rotation = 0,
Iterable<Component>? children,
required this.bloc,
}) : super(
children: [
MultiballBlinkingBehavior(),
MultiballSpriteGroupComponent(
position: position,
litAssetPath: Assets.images.multiball.lit.keyName,
dimmedAssetPath: Assets.images.multiball.dimmed.keyName,
rotation: rotation,
state: bloc.state.lightState,
),
...?children,
],
);
/// {@macro multiball}
Multiball.a({
Iterable<Component>? children,
}) : this._(
position: Vector2(-23, 7.5),
rotation: -24 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// {@macro multiball}
Multiball.b({
Iterable<Component>? children,
}) : this._(
position: Vector2(-7.2, -6.2),
rotation: -5 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// {@macro multiball}
Multiball.c({
Iterable<Component>? children,
}) : this._(
position: Vector2(-0.7, -9.3),
rotation: 2.7 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// {@macro multiball}
Multiball.d({
Iterable<Component>? children,
}) : this._(
position: Vector2(15, 7),
rotation: 24 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// Creates an [Multiball] without any children.
///
/// This can be used for testing [Multiball]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
Multiball.test({
required this.bloc,
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final MultiballCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
}
/// {@template multiball_sprite_group_component}
/// A [SpriteGroupComponent] for the multiball over the board.
/// {@endtemplate}
@visibleForTesting
class MultiballSpriteGroupComponent
extends SpriteGroupComponent<MultiballLightState>
with HasGameRef, ParentIsA<Multiball> {
/// {@macro multiball_sprite_group_component}
MultiballSpriteGroupComponent({
required Vector2 position,
required String litAssetPath,
required String dimmedAssetPath,
required double rotation,
required MultiballLightState state,
}) : _litAssetPath = litAssetPath,
_dimmedAssetPath = dimmedAssetPath,
super(
anchor: Anchor.center,
position: position,
angle: rotation,
current: state,
);
final String _litAssetPath;
final String _dimmedAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state.lightState);
final sprites = {
MultiballLightState.lit: Sprite(
gameRef.images.fromCache(_litAssetPath),
),
MultiballLightState.dimmed:
Sprite(gameRef.images.fromCache(_dimmedAssetPath)),
};
this.sprites = sprites;
size = sprites[current]!.originalSize / 10;
}
}

@ -68,6 +68,14 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
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() {
body.linearVelocity = Vector2(0, 7);
@ -79,11 +87,26 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition].
void release() {
_pullingDownTime = 0;
final velocity = (initialPosition.y - body.position.y) * 11;
body.linearVelocity = Vector2(0, velocity);
_spriteComponent.release();
}
@override
void update(double dt) {
// Ensure that we only pull or release when the time is greater than zero.
if (_pullingDownTime > 0) {
_pullingDownTime -= 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 {

@ -81,6 +81,7 @@ flutter:
- assets/images/google_word/letter5/
- assets/images/google_word/letter6/
- assets/images/signpost/
- assets/images/multiball/
- assets/images/multiplier/x2/
- assets/images/multiplier/x3/
- assets/images/multiplier/x4/

@ -27,6 +27,7 @@ void main() {
addScoreStories(dashbook);
addBackboardStories(dashbook);
addDinoWallStories(dashbook);
addMultiballStories(dashbook);
addMultipliersStories(dashbook);
runApp(dashbook);

@ -0,0 +1,56 @@
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class MultiballGame extends BallGame with KeyboardEvents {
MultiballGame()
: super(
imagesFileNames: [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
],
);
static const description = '''
Shows how the Multiball are rendered.
- Tap anywhere on the screen to spawn a ball into the game.
- Press space bar to animate multiballs.
''';
final List<Multiball> multiballs = [
Multiball.a(),
Multiball.b(),
Multiball.c(),
Multiball.d(),
];
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await addAll(multiballs);
await traceAllBodies();
}
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) {
for (final multiball in multiballs) {
multiball.bloc.onBlink();
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
}

@ -0,0 +1,11 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/multiball/multiball_game.dart';
void addMultiballStories(Dashbook dashbook) {
dashbook.storiesOf('Multiball').addGame(
title: 'Assets',
description: MultiballGame.description,
gameBuilder: (_) => MultiballGame(),
);
}

@ -10,6 +10,7 @@ export 'flutter_forest/stories.dart';
export 'google_word/stories.dart';
export 'launch_ramp/stories.dart';
export 'layer/stories.dart';
export 'multiball/stories.dart';
export 'multipliers/stories.dart';
export 'plunger/stories.dart';
export 'score/stories.dart';

@ -25,6 +25,8 @@ class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
class MockMultiballCubit extends Mock implements MultiballCubit {}
class MockMultiplierCubit extends Mock implements MultiplierCubit {}
class MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}

@ -0,0 +1,158 @@
// ignore_for_file: prefer_const_constructors, cascade_invocations
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_components/pinball_components.dart';
import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'MultiballBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls onBlink every 0.1 seconds when animation state is animated',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
game.update(0);
verify(bloc.onBlink).called(1);
await tester.pump();
game.update(0.1);
await streamController.close();
verify(bloc.onBlink).called(1);
},
);
flameTester.testGameWidget(
'calls onStop when animation state is stopped',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
streamController.add(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.lit,
),
);
await streamController.close();
verify(bloc.onStop).called(1);
},
);
flameTester.testGameWidget(
'onTick stops when there is no animation',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
behavior.onTick();
expect(behavior.timer.isRunning(), false);
},
);
flameTester.testGameWidget(
'onTick stops after 10 blinks repetitions',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.dimmed,
),
);
await tester.pump();
for (var i = 0; i < 10; i++) {
behavior.onTick();
}
expect(behavior.timer.isRunning(), false);
},
);
},
);
}

@ -0,0 +1,67 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'MultiballCubit',
() {
blocTest<MultiballCubit, MultiballState>(
'onAnimate emits animationState [animate]',
build: MultiballCubit.new,
act: (bloc) => bloc.onAnimate(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.animationState,
'animationState',
MultiballAnimationState.blinking,
)
],
);
blocTest<MultiballCubit, MultiballState>(
'onStop emits animationState [stopped]',
build: MultiballCubit.new,
act: (bloc) => bloc.onStop(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.animationState,
'animationState',
MultiballAnimationState.idle,
)
],
);
blocTest<MultiballCubit, MultiballState>(
'onBlink emits lightState [lit, dimmed, lit]',
build: MultiballCubit.new,
act: (bloc) => bloc
..onBlink()
..onBlink()
..onBlink(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.lightState,
'lightState',
MultiballLightState.lit,
),
isA<MultiballState>()
..having(
(state) => state.lightState,
'lightState',
MultiballLightState.dimmed,
),
isA<MultiballState>()
..having(
(state) => state.lightState,
'lightState',
MultiballLightState.lit,
)
],
);
},
);
}

@ -0,0 +1,76 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/pinball_components.dart';
void main() {
group('MultiballState', () {
test('supports value equality', () {
expect(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
),
equals(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
),
isNotNull,
);
});
});
group('copyWith', () {
test(
'copies correctly '
'when no argument specified',
() {
final multiballState = MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
);
expect(
multiballState.copyWith(),
equals(multiballState),
);
},
);
test(
'copies correctly '
'when all arguments specified',
() {
final multiballState = MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
);
final otherMultiballState = MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
);
expect(multiballState, isNot(equals(otherMultiballState)));
expect(
multiballState.copyWith(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
equals(otherMultiballState),
);
},
);
});
});
}

@ -0,0 +1,90 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('Multiball', () {
group('loads correctly', () {
flameTester.test('"a"', (game) async {
final multiball = Multiball.a();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
flameTester.test('"b"', (game) async {
final multiball = Multiball.b();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
flameTester.test('"c"', (game) async {
final multiball = Multiball.c();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
flameTester.test('"d"', (game) async {
final multiball = Multiball.d();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
});
flameTester.test(
'closes bloc when removed',
(game) async {
final bloc = MockMultiballCubit();
whenListen(
bloc,
const Stream<MultiballLightState>.empty(),
initialState: MultiballLightState.dimmed,
);
when(bloc.close).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await game.ensureAdd(multiball);
game.remove(multiball);
await game.ready();
verify(bloc.close).called(1);
},
);
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final multiball = Multiball.a(
children: [component],
);
await game.ensureAdd(multiball);
expect(multiball.children, contains(component));
});
flameTester.test('a MultiballBlinkingBehavior', (game) async {
final multiball = Multiball.a();
await game.ensureAdd(multiball);
expect(
multiball.children.whereType<MultiballBlinkingBehavior>().single,
isNotNull,
);
});
});
});
}

@ -121,6 +121,33 @@ void main() {
);
});
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);
await tester.pump(const Duration(seconds: 2));
expect(plunger.body.linearVelocity.y, isZero);
},
);
});
group('pull', () {
late Plunger plunger;

@ -0,0 +1,9 @@
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{folder}/{imageId} {
allow read: if imageId.matches(".*\\.png") || imageId.matches(".*\\.jpg");
allow write: if false;
}
}
}

@ -0,0 +1,136 @@
// 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/components/multiballs/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
];
group('MultiballsBehavior', () {
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,
);
group('listenWhen', () {
test(
'is true when the bonusHistory has changed '
'with a new GameBonus.dashNest', () {
final previous = GameState.initial();
final state = previous.copyWith(
bonusHistory: [GameBonus.dashNest],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isTrue,
);
});
test(
'is false when the bonusHistory has changed '
'with a bonus different than GameBonus.dashNest', () {
final previous =
GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]);
final state = previous.copyWith(
bonusHistory: [...previous.bonusHistory, GameBonus.androidSpaceship],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isFalse,
);
});
test('is false when the bonusHistory state is the same', () {
final previous = GameState.initial();
final state = GameState(
score: 10,
multiplier: 1,
rounds: 0,
bonusHistory: const [],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isFalse,
);
});
});
group('onNewState', () {
flameBlocTester.testGameWidget(
"calls 'onAnimate' once for every multiball",
setUp: (game, tester) async {
final behavior = MultiballsBehavior();
final parent = Multiballs.test();
final multiballCubit = MockMultiballCubit();
final otherMultiballCubit = MockMultiballCubit();
final multiballs = [
Multiball.test(
bloc: multiballCubit,
),
Multiball.test(
bloc: otherMultiballCubit,
),
];
whenListen(
multiballCubit,
const Stream<MultiballState>.empty(),
initialState: MultiballState.initial(),
);
when(multiballCubit.onAnimate).thenAnswer((_) async {});
whenListen(
otherMultiballCubit,
const Stream<MultiballState>.empty(),
initialState: MultiballState.initial(),
);
when(otherMultiballCubit.onAnimate).thenAnswer((_) async {});
await parent.addAll(multiballs);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
await tester.pump();
behavior.onNewState(
GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]),
);
for (final multiball in multiballs) {
verify(
multiball.bloc.onAnimate,
).called(1);
}
},
);
});
});
}

@ -0,0 +1,54 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
];
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
group('Multiballs', () {
flameBlocTester.testGameWidget(
'loads correctly',
setUp: (game, tester) async {
final multiballs = Multiballs();
await game.ensureAdd(multiballs);
expect(game.contains(multiballs), isTrue);
},
);
group('loads', () {
flameBlocTester.testGameWidget(
'four Multiball',
setUp: (game, tester) async {
final multiballs = Multiballs();
await game.ensureAdd(multiballs);
expect(
multiballs.descendants().whereType<Multiball>().length,
equals(4),
);
},
);
});
});
}

@ -64,6 +64,8 @@ void main() {
Assets.images.launchRamp.ramp.keyName,
Assets.images.launchRamp.foregroundRailing.keyName,
Assets.images.launchRamp.backgroundRailing.keyName,
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName,
@ -178,6 +180,18 @@ void main() {
);
});
flameBlocTester.test(
'has only one Multiballs',
(game) async {
await game.ready();
expect(
game.descendants().whereType<Multiballs>().length,
equals(1),
);
},
);
flameBlocTester.test(
'one GoogleWord',
(game) async {
@ -400,54 +414,9 @@ void main() {
game.onTapDown(tapDownEvent);
expect(plunger.body.linearVelocity.y, equals(7));
});
flameTester.test('tap up releases plunger', (game) async {
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2(40, 60));
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final plunger = game.descendants().whereType<Plunger>().first;
game.onTapDown(tapDownEvent);
expect(plunger.body.linearVelocity.y, equals(7));
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
game.onTapUp(tapUpEvent);
expect(plunger.body.linearVelocity.y, equals(0));
});
flameTester.test('tap cancel releases plunger', (game) async {
await game.ready();
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2(40, 60));
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final plunger = game.descendants().whereType<Plunger>().first;
game.onTapDown(tapDownEvent);
expect(plunger.body.linearVelocity.y, equals(7));
game.onTapCancel();
game.update(1);
expect(plunger.body.linearVelocity.y, equals(0));
expect(plunger.body.linearVelocity.y, isPositive);
});
});
});

@ -95,9 +95,7 @@ class MockAndroidBumper extends Mock implements AndroidBumper {}
class MockSparkyBumper extends Mock implements SparkyBumper {}
class MockMultiplier extends Mock implements Multiplier {}
class MockMultipliersGroup extends Mock implements Multipliers {}
class MockMultiballCubit extends Mock implements MultiballCubit {}
class MockMultiplierCubit extends Mock implements MultiplierCubit {}

@ -73,5 +73,25 @@ void main() {
await tester.pumpAndSettle();
expect(find.byType(HowToPlayDialog), findsNothing);
});
testWidgets('can be dismissed', (tester) async {
await tester.pumpApp(
Builder(
builder: (context) {
return TextButton(
onPressed: () => showHowToPlayDialog(context),
child: const Text('test'),
);
},
),
);
expect(find.byType(HowToPlayDialog), findsNothing);
await tester.tap(find.text('test'));
await tester.pumpAndSettle();
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
expect(find.byType(HowToPlayDialog), findsNothing);
});
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 917 B

@ -24,27 +24,22 @@
<meta property="og:description"
content="Come play Pinball with your favorite Google Developer Mascots! Built with Flutter & Firebase for Google I/O 2022.">
<!-- Open Graph Data -->
<meta property="og:title" content="Google I/O Pinball">
<!-- TODO(jonathandaniels-vgv): revisit once Google sets up deployments -->
<meta property="og:url" content="https://flutter.dev">
<!-- TODO(jonathandaniels-vgv): swap this image with updated pinball image -->
<meta name="twitter:image"
content="https://firebasestorage.googleapis.com/v0/b/io-photobooth-dev.appspot.com/o/public%2Fphotobooth-metadata-image.jpeg?alt=media">
<!-- TODO(jonathandaniels-vgv): swap this image with updated pinball image -->
<meta property="og:image"
content="https://firebasestorage.googleapis.com/v0/b/io-photobooth-dev.appspot.com/o/public%2Fphotobooth-metadata-image.jpeg?alt=media">
content="https://firebasestorage.googleapis.com/v0/b/pinball-dev.appspot.com/o/images%2Fpinball_share_image.png?alt=media">
<!-- Twitter Share Data -->
<meta name="twitter:image"
content="https://firebasestorage.googleapis.com/v0/b/pinball-dev.appspot.com/o/images%2Fpinball_share_image.png?alt=media">
<meta name="twitter:text:title" content="I/O Pinball Machine - Flutter">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="I/O Pinball Machine - Flutter">
<meta name="twitter:description"
content="Come play Pinball with your favorite Google Developer Mascots! Built with Flutter & Firebase for Google I/O 2022.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">

Loading…
Cancel
Save