Merge branch 'main' into refactor/spaceship-ramp-curve

pull/173/head
Allison Ryan 4 years ago
commit 2fe323145c

@ -0,0 +1,95 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template alien_zone}
/// Area positioned below [Spaceship] where the [Ball]
/// can bounce off [AlienBumper]s.
///
/// When a [Ball] hits [AlienBumper]s, they toggle between activated and
/// deactivated states.
/// {@endtemplate}
class AlienZone extends Component with HasGameRef<PinballGame> {
/// {@macro alien_zone}
AlienZone();
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_ControlledAlienBumperBallContactCallback());
final lowerBumper = ControlledAlienBumper.a()
..initialPosition = Vector2(-32.52, 9.34);
final upperBumper = ControlledAlienBumper.b()
..initialPosition = Vector2(-22.89, 17.43);
await addAll([
lowerBumper,
upperBumper,
]);
}
}
/// {@template controlled_alien_bumper}
/// [AlienBumper] with [_AlienBumperController] attached.
/// {@endtemplate}
@visibleForTesting
class ControlledAlienBumper extends AlienBumper
with Controls<_AlienBumperController>, ScorePoints {
/// {@macro controlled_alien_bumper}
ControlledAlienBumper.a() : super.a() {
controller = _AlienBumperController(this);
}
/// {@macro controlled_alien_bumper}
ControlledAlienBumper.b() : super.b() {
controller = _AlienBumperController(this);
}
@override
// TODO(ruimiguel): change points when get final points map.
int get points => 20;
}
/// {@template alien_bumper_controller}
/// Controls a [AlienBumper].
/// {@endtemplate}
class _AlienBumperController extends ComponentController<AlienBumper>
with HasGameRef<PinballGame> {
/// {@macro alien_bumper_controller}
_AlienBumperController(AlienBumper alienBumper) : super(alienBumper);
/// Flag for activated state of the [AlienBumper].
///
/// Used to toggle [AlienBumper]s' state between activated and deactivated.
bool isActivated = false;
/// Registers when a [AlienBumper] is hit by a [Ball].
void hit() {
if (isActivated) {
component.deactivate();
} else {
component.activate();
}
isActivated = !isActivated;
}
}
/// Listens when a [Ball] bounces bounces against a [AlienBumper].
class _ControlledAlienBumperBallContactCallback
extends ContactCallback<Controls<_AlienBumperController>, Ball> {
@override
void begin(
Controls<_AlienBumperController> controlledAlienBumper,
Ball _,
Contact __,
) {
controlledAlienBumper.controller.hit();
}
}

@ -1,12 +1,14 @@
export 'alien_zone.dart';
export 'board.dart'; export 'board.dart';
export 'bonus_word.dart'; export 'bonus_word.dart';
export 'camera_controller.dart'; export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'controlled_sparky_computer.dart'; export 'controlled_sparky_computer.dart';
export 'flutter_forest.dart'; export 'flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_flow_controller.dart';
export 'plunger.dart'; export 'score_effect_controller.dart';
export 'score_points.dart'; export 'score_points.dart';
export 'sparky_fire_zone.dart'; export 'sparky_fire_zone.dart';
export 'wall.dart'; export 'wall.dart';

@ -0,0 +1,49 @@
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball_components/pinball_components.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);
}
}
/// {@template plunger_controller}
/// A [ComponentController] that controls a [Plunger]s movement.
/// {@endtemplate}
class PlungerController extends ComponentController<Plunger>
with KeyboardHandler {
/// {@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 (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
component.pull();
} else if (event is RawKeyUpEvent) {
component.release();
}
return false;
}
}

@ -37,12 +37,14 @@ class FlutterForest extends Component with Controls<_FlutterForestController> {
final smallRightNest = _ControlledSmallDashNestBumper.b( final smallRightNest = _ControlledSmallDashNestBumper.b(
id: 'small_nest_bumper_b', id: 'small_nest_bumper_b',
)..initialPosition = Vector2(23.3, 46.75); )..initialPosition = Vector2(23.3, 46.75);
final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66);
await addAll([ await addAll([
signPost, signPost,
smallLeftNest, smallLeftNest,
smallRightNest, smallRightNest,
bigNest, bigNest,
dashAnimatronic,
]); ]);
} }
} }
@ -68,7 +70,13 @@ class _FlutterForestController extends ComponentController<FlutterForest>
void onNewState(GameState state) { void onNewState(GameState state) {
super.onNewState(state); super.onNewState(state);
gameRef.add( component.firstChild<DashAnimatronic>()?.playing = true;
_addBonusBall();
}
Future<void> _addBonusBall() async {
await Future<void>.delayed(const Duration(milliseconds: 700));
await gameRef.add(
ControlledBall.bonus(theme: gameRef.theme) ControlledBall.bonus(theme: gameRef.theme)
..initialPosition = Vector2(17.2, 52.7), ..initialPosition = Vector2(17.2, 52.7),
); );

@ -0,0 +1,45 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template score_effect_controller}
/// A [ComponentController] responsible for adding [ScoreText]s
/// on the game screen when the user earns points.
/// {@endtemplate}
class ScoreEffectController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState> {
/// {@macro score_effect_controller}
ScoreEffectController(PinballGame component) : super(component);
int _lastScore = 0;
final _random = Random();
double _noise() {
return _random.nextDouble() * 5 * (_random.nextBool() ? -1 : 1);
}
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.score != newState.score;
}
@override
void onNewState(GameState state) {
final newScore = state.score - _lastScore;
_lastScore = state.score;
component.add(
ScoreText(
text: newScore.toString(),
position: Vector2(
_noise(),
_noise() + (-BoardDimensions.bounds.topCenter.dy + 10),
),
),
);
}
}

@ -25,12 +25,13 @@ extension PinballGameAssetsX on PinballGame {
), ),
images.load(components.Assets.images.dino.dinoLandTop.keyName), images.load(components.Assets.images.dino.dinoLandTop.keyName),
images.load(components.Assets.images.dino.dinoLandBottom.keyName), images.load(components.Assets.images.dino.dinoLandBottom.keyName),
images.load(components.Assets.images.dashBumper.a.active.keyName), images.load(components.Assets.images.dash.animatronic.keyName),
images.load(components.Assets.images.dashBumper.a.inactive.keyName), images.load(components.Assets.images.dash.bumper.a.active.keyName),
images.load(components.Assets.images.dashBumper.b.active.keyName), images.load(components.Assets.images.dash.bumper.a.inactive.keyName),
images.load(components.Assets.images.dashBumper.b.inactive.keyName), images.load(components.Assets.images.dash.bumper.b.active.keyName),
images.load(components.Assets.images.dashBumper.main.active.keyName), images.load(components.Assets.images.dash.bumper.b.inactive.keyName),
images.load(components.Assets.images.dashBumper.main.inactive.keyName), images.load(components.Assets.images.dash.bumper.main.active.keyName),
images.load(components.Assets.images.dash.bumper.main.inactive.keyName),
images.load(components.Assets.images.boundary.bottom.keyName), images.load(components.Assets.images.boundary.bottom.keyName),
images.load(components.Assets.images.boundary.outer.keyName), images.load(components.Assets.images.boundary.outer.keyName),
images.load(components.Assets.images.spaceship.saucer.keyName), images.load(components.Assets.images.spaceship.saucer.keyName),
@ -47,6 +48,7 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.spaceship.rail.foreground.keyName), images.load(components.Assets.images.spaceship.rail.foreground.keyName),
images.load(components.Assets.images.chromeDino.mouth.keyName), images.load(components.Assets.images.chromeDino.mouth.keyName),
images.load(components.Assets.images.chromeDino.head.keyName), images.load(components.Assets.images.chromeDino.head.keyName),
images.load(components.Assets.images.plunger.plunger.keyName),
images.load(components.Assets.images.sparky.computer.base.keyName), images.load(components.Assets.images.sparky.computer.base.keyName),
images.load(components.Assets.images.sparky.computer.top.keyName), images.load(components.Assets.images.sparky.computer.top.keyName),
images.load(components.Assets.images.sparky.bumper.a.active.keyName), images.load(components.Assets.images.sparky.bumper.a.active.keyName),

@ -38,6 +38,7 @@ class PinballGame extends Forge2DGame
Future<void> onLoad() async { Future<void> onLoad() async {
_addContactCallbacks(); _addContactCallbacks();
unawaited(add(ScoreEffectController(this)));
unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this))); unawaited(add(CameraController(this)));
unawaited(add(Backboard(position: Vector2(0, -88)))); unawaited(add(Backboard(position: Vector2(0, -88))));
@ -47,11 +48,12 @@ class PinballGame extends Forge2DGame
unawaited(addFromBlueprint(LaunchRamp())); unawaited(addFromBlueprint(LaunchRamp()));
unawaited(addFromBlueprint(ControlledSparkyComputer())); unawaited(addFromBlueprint(ControlledSparkyComputer()));
final plunger = Plunger(compressionDistance: 29) final plunger = ControlledPlunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19); ..initialPosition = Vector2(38, -19);
await add(plunger); await add(plunger);
unawaited(add(Board())); unawaited(add(Board()));
unawaited(add(AlienZone()));
unawaited(add(SparkyFireZone())); unawaited(add(SparkyFireZone()));
unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls())); unawaited(addFromBlueprint(DinoWalls()));

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -3,36 +3,58 @@
/// FlutterGen /// FlutterGen
/// ***************************************************** /// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
$AssetsImagesAlienBumperGen get alienBumper =>
const $AssetsImagesAlienBumperGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
/// File path: assets/images/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
$AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen();
$AssetsImagesChromeDinoGen get chromeDino => $AssetsImagesChromeDinoGen get chromeDino =>
const $AssetsImagesChromeDinoGen(); const $AssetsImagesChromeDinoGen();
$AssetsImagesDashBumperGen get dashBumper => $AssetsImagesDashGen get dash => const $AssetsImagesDashGen();
const $AssetsImagesDashBumperGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
/// File path: assets/images/flutter_sign_post.png
AssetGenImage get flutterSignPost => AssetGenImage get flutterSignPost =>
const AssetGenImage('assets/images/flutter_sign_post.png'); const AssetGenImage('assets/images/flutter_sign_post.png');
$AssetsImagesGoogleWordGen get googleWord =>
const $AssetsImagesGoogleWordGen();
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp => $AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
} }
class $AssetsImagesAlienBumperGen {
const $AssetsImagesAlienBumperGen();
$AssetsImagesAlienBumperAGen get a => const $AssetsImagesAlienBumperAGen();
$AssetsImagesAlienBumperBGen get b => const $AssetsImagesAlienBumperBGen();
}
class $AssetsImagesBackboardGen { class $AssetsImagesBackboardGen {
const $AssetsImagesBackboardGen(); const $AssetsImagesBackboardGen();
/// File path: assets/images/backboard/backboard_game_over.png
AssetGenImage get backboardGameOver => AssetGenImage get backboardGameOver =>
const AssetGenImage('assets/images/backboard/backboard_game_over.png'); const AssetGenImage('assets/images/backboard/backboard_game_over.png');
/// File path: assets/images/backboard/backboard_scores.png
AssetGenImage get backboardScores => AssetGenImage get backboardScores =>
const AssetGenImage('assets/images/backboard/backboard_scores.png'); const AssetGenImage('assets/images/backboard/backboard_scores.png');
} }
@ -40,8 +62,11 @@ class $AssetsImagesBackboardGen {
class $AssetsImagesBaseboardGen { class $AssetsImagesBaseboardGen {
const $AssetsImagesBaseboardGen(); const $AssetsImagesBaseboardGen();
/// File path: assets/images/baseboard/left.png
AssetGenImage get left => AssetGenImage get left =>
const AssetGenImage('assets/images/baseboard/left.png'); const AssetGenImage('assets/images/baseboard/left.png');
/// File path: assets/images/baseboard/right.png
AssetGenImage get right => AssetGenImage get right =>
const AssetGenImage('assets/images/baseboard/right.png'); const AssetGenImage('assets/images/baseboard/right.png');
} }
@ -49,8 +74,11 @@ class $AssetsImagesBaseboardGen {
class $AssetsImagesBoundaryGen { class $AssetsImagesBoundaryGen {
const $AssetsImagesBoundaryGen(); const $AssetsImagesBoundaryGen();
/// File path: assets/images/boundary/bottom.png
AssetGenImage get bottom => AssetGenImage get bottom =>
const AssetGenImage('assets/images/boundary/bottom.png'); const AssetGenImage('assets/images/boundary/bottom.png');
/// File path: assets/images/boundary/outer.png
AssetGenImage get outer => AssetGenImage get outer =>
const AssetGenImage('assets/images/boundary/outer.png'); const AssetGenImage('assets/images/boundary/outer.png');
} }
@ -58,26 +86,33 @@ class $AssetsImagesBoundaryGen {
class $AssetsImagesChromeDinoGen { class $AssetsImagesChromeDinoGen {
const $AssetsImagesChromeDinoGen(); const $AssetsImagesChromeDinoGen();
/// File path: assets/images/chrome_dino/head.png
AssetGenImage get head => AssetGenImage get head =>
const AssetGenImage('assets/images/chrome_dino/head.png'); const AssetGenImage('assets/images/chrome_dino/head.png');
/// File path: assets/images/chrome_dino/mouth.png
AssetGenImage get mouth => AssetGenImage get mouth =>
const AssetGenImage('assets/images/chrome_dino/mouth.png'); const AssetGenImage('assets/images/chrome_dino/mouth.png');
} }
class $AssetsImagesDashBumperGen { class $AssetsImagesDashGen {
const $AssetsImagesDashBumperGen(); const $AssetsImagesDashGen();
$AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen(); /// File path: assets/images/dash/animatronic.png
$AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen(); AssetGenImage get animatronic =>
$AssetsImagesDashBumperMainGen get main => const AssetGenImage('assets/images/dash/animatronic.png');
const $AssetsImagesDashBumperMainGen();
$AssetsImagesDashBumperGen get bumper => const $AssetsImagesDashBumperGen();
} }
class $AssetsImagesDinoGen { class $AssetsImagesDinoGen {
const $AssetsImagesDinoGen(); const $AssetsImagesDinoGen();
/// File path: assets/images/dino/dino-land-bottom.png
AssetGenImage get dinoLandBottom => AssetGenImage get dinoLandBottom =>
const AssetGenImage('assets/images/dino/dino-land-bottom.png'); const AssetGenImage('assets/images/dino/dino-land-bottom.png');
/// File path: assets/images/dino/dino-land-top.png
AssetGenImage get dinoLandTop => AssetGenImage get dinoLandTop =>
const AssetGenImage('assets/images/dino/dino-land-top.png'); const AssetGenImage('assets/images/dino/dino-land-top.png');
} }
@ -85,17 +120,51 @@ class $AssetsImagesDinoGen {
class $AssetsImagesFlipperGen { class $AssetsImagesFlipperGen {
const $AssetsImagesFlipperGen(); const $AssetsImagesFlipperGen();
/// File path: assets/images/flipper/left.png
AssetGenImage get left => AssetGenImage get left =>
const AssetGenImage('assets/images/flipper/left.png'); const AssetGenImage('assets/images/flipper/left.png');
/// File path: assets/images/flipper/right.png
AssetGenImage get right => AssetGenImage get right =>
const AssetGenImage('assets/images/flipper/right.png'); const AssetGenImage('assets/images/flipper/right.png');
} }
class $AssetsImagesGoogleWordGen {
const $AssetsImagesGoogleWordGen();
/// File path: assets/images/google_word/letter1.png
AssetGenImage get letter1 =>
const AssetGenImage('assets/images/google_word/letter1.png');
/// File path: assets/images/google_word/letter2.png
AssetGenImage get letter2 =>
const AssetGenImage('assets/images/google_word/letter2.png');
/// File path: assets/images/google_word/letter3.png
AssetGenImage get letter3 =>
const AssetGenImage('assets/images/google_word/letter3.png');
/// File path: assets/images/google_word/letter4.png
AssetGenImage get letter4 =>
const AssetGenImage('assets/images/google_word/letter4.png');
/// File path: assets/images/google_word/letter5.png
AssetGenImage get letter5 =>
const AssetGenImage('assets/images/google_word/letter5.png');
/// File path: assets/images/google_word/letter6.png
AssetGenImage get letter6 =>
const AssetGenImage('assets/images/google_word/letter6.png');
}
class $AssetsImagesKickerGen { class $AssetsImagesKickerGen {
const $AssetsImagesKickerGen(); const $AssetsImagesKickerGen();
/// File path: assets/images/kicker/left.png
AssetGenImage get left => AssetGenImage get left =>
const AssetGenImage('assets/images/kicker/left.png'); const AssetGenImage('assets/images/kicker/left.png');
/// File path: assets/images/kicker/right.png
AssetGenImage get right => AssetGenImage get right =>
const AssetGenImage('assets/images/kicker/right.png'); const AssetGenImage('assets/images/kicker/right.png');
} }
@ -103,21 +172,39 @@ class $AssetsImagesKickerGen {
class $AssetsImagesLaunchRampGen { class $AssetsImagesLaunchRampGen {
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
/// File path: assets/images/launch_ramp/foreground-railing.png
AssetGenImage get foregroundRailing => AssetGenImage get foregroundRailing =>
const AssetGenImage('assets/images/launch_ramp/foreground-railing.png'); const AssetGenImage('assets/images/launch_ramp/foreground-railing.png');
/// File path: assets/images/launch_ramp/ramp.png
AssetGenImage get ramp => AssetGenImage get ramp =>
const AssetGenImage('assets/images/launch_ramp/ramp.png'); const AssetGenImage('assets/images/launch_ramp/ramp.png');
} }
class $AssetsImagesPlungerGen {
const $AssetsImagesPlungerGen();
/// File path: assets/images/plunger/plunger.png
AssetGenImage get plunger =>
const AssetGenImage('assets/images/plunger/plunger.png');
}
class $AssetsImagesSlingshotGen { class $AssetsImagesSlingshotGen {
const $AssetsImagesSlingshotGen(); const $AssetsImagesSlingshotGen();
/// File path: assets/images/slingshot/left_lower.png
AssetGenImage get leftLower => AssetGenImage get leftLower =>
const AssetGenImage('assets/images/slingshot/left_lower.png'); const AssetGenImage('assets/images/slingshot/left_lower.png');
/// File path: assets/images/slingshot/left_upper.png
AssetGenImage get leftUpper => AssetGenImage get leftUpper =>
const AssetGenImage('assets/images/slingshot/left_upper.png'); const AssetGenImage('assets/images/slingshot/left_upper.png');
/// File path: assets/images/slingshot/right_lower.png
AssetGenImage get rightLower => AssetGenImage get rightLower =>
const AssetGenImage('assets/images/slingshot/right_lower.png'); const AssetGenImage('assets/images/slingshot/right_lower.png');
/// File path: assets/images/slingshot/right_upper.png
AssetGenImage get rightUpper => AssetGenImage get rightUpper =>
const AssetGenImage('assets/images/slingshot/right_upper.png'); const AssetGenImage('assets/images/slingshot/right_upper.png');
} }
@ -125,12 +212,16 @@ class $AssetsImagesSlingshotGen {
class $AssetsImagesSpaceshipGen { class $AssetsImagesSpaceshipGen {
const $AssetsImagesSpaceshipGen(); const $AssetsImagesSpaceshipGen();
/// File path: assets/images/spaceship/bridge.png
AssetGenImage get bridge => AssetGenImage get bridge =>
const AssetGenImage('assets/images/spaceship/bridge.png'); const AssetGenImage('assets/images/spaceship/bridge.png');
$AssetsImagesSpaceshipRailGen get rail => $AssetsImagesSpaceshipRailGen get rail =>
const $AssetsImagesSpaceshipRailGen(); const $AssetsImagesSpaceshipRailGen();
$AssetsImagesSpaceshipRampGen get ramp => $AssetsImagesSpaceshipRampGen get ramp =>
const $AssetsImagesSpaceshipRampGen(); const $AssetsImagesSpaceshipRampGen();
/// File path: assets/images/spaceship/saucer.png
AssetGenImage get saucer => AssetGenImage get saucer =>
const AssetGenImage('assets/images/spaceship/saucer.png'); const AssetGenImage('assets/images/spaceship/saucer.png');
} }
@ -144,38 +235,47 @@ class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyComputerGen(); const $AssetsImagesSparkyComputerGen();
} }
class $AssetsImagesDashBumperAGen { class $AssetsImagesAlienBumperAGen {
const $AssetsImagesDashBumperAGen(); const $AssetsImagesAlienBumperAGen();
/// File path: assets/images/alien_bumper/a/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/dash_bumper/a/active.png'); const AssetGenImage('assets/images/alien_bumper/a/active.png');
/// File path: assets/images/alien_bumper/a/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash_bumper/a/inactive.png'); const AssetGenImage('assets/images/alien_bumper/a/inactive.png');
} }
class $AssetsImagesDashBumperBGen { class $AssetsImagesAlienBumperBGen {
const $AssetsImagesDashBumperBGen(); const $AssetsImagesAlienBumperBGen();
/// File path: assets/images/alien_bumper/b/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/dash_bumper/b/active.png'); const AssetGenImage('assets/images/alien_bumper/b/active.png');
/// File path: assets/images/alien_bumper/b/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash_bumper/b/inactive.png'); const AssetGenImage('assets/images/alien_bumper/b/inactive.png');
} }
class $AssetsImagesDashBumperMainGen { class $AssetsImagesDashBumperGen {
const $AssetsImagesDashBumperMainGen(); const $AssetsImagesDashBumperGen();
AssetGenImage get active => $AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen();
const AssetGenImage('assets/images/dash_bumper/main/active.png'); $AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen();
AssetGenImage get inactive => $AssetsImagesDashBumperMainGen get main =>
const AssetGenImage('assets/images/dash_bumper/main/inactive.png'); const $AssetsImagesDashBumperMainGen();
} }
class $AssetsImagesSpaceshipRailGen { class $AssetsImagesSpaceshipRailGen {
const $AssetsImagesSpaceshipRailGen(); const $AssetsImagesSpaceshipRailGen();
/// File path: assets/images/spaceship/rail/foreground.png
AssetGenImage get foreground => AssetGenImage get foreground =>
const AssetGenImage('assets/images/spaceship/rail/foreground.png'); const AssetGenImage('assets/images/spaceship/rail/foreground.png');
/// File path: assets/images/spaceship/rail/main.png
AssetGenImage get main => AssetGenImage get main =>
const AssetGenImage('assets/images/spaceship/rail/main.png'); const AssetGenImage('assets/images/spaceship/rail/main.png');
} }
@ -183,12 +283,19 @@ class $AssetsImagesSpaceshipRailGen {
class $AssetsImagesSpaceshipRampGen { class $AssetsImagesSpaceshipRampGen {
const $AssetsImagesSpaceshipRampGen(); const $AssetsImagesSpaceshipRampGen();
/// File path: assets/images/spaceship/ramp/board-opening.png
AssetGenImage get boardOpening => AssetGenImage get boardOpening =>
const AssetGenImage('assets/images/spaceship/ramp/board-opening.png'); const AssetGenImage('assets/images/spaceship/ramp/board-opening.png');
/// File path: assets/images/spaceship/ramp/main.png
AssetGenImage get main => AssetGenImage get main =>
const AssetGenImage('assets/images/spaceship/ramp/main.png'); const AssetGenImage('assets/images/spaceship/ramp/main.png');
/// File path: assets/images/spaceship/ramp/railing-background.png
AssetGenImage get railingBackground => const AssetGenImage( AssetGenImage get railingBackground => const AssetGenImage(
'assets/images/spaceship/ramp/railing-background.png'); 'assets/images/spaceship/ramp/railing-background.png');
/// File path: assets/images/spaceship/ramp/railing-foreground.png
AssetGenImage get railingForeground => const AssetGenImage( AssetGenImage get railingForeground => const AssetGenImage(
'assets/images/spaceship/ramp/railing-foreground.png'); 'assets/images/spaceship/ramp/railing-foreground.png');
} }
@ -213,6 +320,42 @@ class $AssetsImagesSparkyComputerGen {
const AssetGenImage('assets/images/sparky/computer/top.png'); const AssetGenImage('assets/images/sparky/computer/top.png');
} }
class $AssetsImagesDashBumperAGen {
const $AssetsImagesDashBumperAGen();
/// File path: assets/images/dash/bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash/bumper/a/active.png');
/// File path: assets/images/dash/bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash/bumper/a/inactive.png');
}
class $AssetsImagesDashBumperBGen {
const $AssetsImagesDashBumperBGen();
/// File path: assets/images/dash/bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash/bumper/b/active.png');
/// File path: assets/images/dash/bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash/bumper/b/inactive.png');
}
class $AssetsImagesDashBumperMainGen {
const $AssetsImagesDashBumperMainGen();
/// File path: assets/images/dash/bumper/main/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash/bumper/main/active.png');
/// File path: assets/images/dash/bumper/main/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash/bumper/main/inactive.png');
}
class $AssetsImagesSparkyBumperAGen { class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen(); const $AssetsImagesSparkyBumperAGen();

@ -0,0 +1,16 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
class FontFamily {
FontFamily._();
/// Font family: PixeloidMono
static const String pixeloidMono = 'PixeloidMono';
/// Font family: PixeloidSans
static const String pixeloidSans = 'PixeloidSans';
}

@ -0,0 +1,16 @@
import 'package:pinball_components/gen/fonts.gen.dart';
String _prefixFont(String font) {
return 'packages/pinball_components/$font';
}
/// Class with the fonts available on the pinball game
class PinballFonts {
PinballFonts._();
/// Mono variation of the Pixeloid font
static final String pixeloidMono = _prefixFont(FontFamily.pixeloidMono);
/// Sans variation of the Pixeloid font
static final String pixeloidSans = _prefixFont(FontFamily.pixeloidMono);
}

@ -1,4 +1,5 @@
library pinball_components; library pinball_components;
export 'gen/assets.gen.dart'; export 'gen/assets.gen.dart';
export 'gen/pinball_fonts.dart';
export 'src/pinball_components.dart'; export 'src/pinball_components.dart';

@ -0,0 +1,109 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template alien_bumper}
/// Bumper for Alien area.
/// {@endtemplate}
// TODO(ruimiguel): refactor later to unify with DashBumpers.
class AlienBumper extends BodyComponent with InitialPosition {
/// {@macro alien_bumper}
AlienBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
_activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent;
/// {@macro alien_bumper}
AlienBumper.a()
: this._(
majorRadius: 3.52,
minorRadius: 2.97,
activeAssetPath: Assets.images.alienBumper.a.active.keyName,
inactiveAssetPath: Assets.images.alienBumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.1),
),
);
/// {@macro alien_bumper}
AlienBumper.b()
: this._(
majorRadius: 3.19,
minorRadius: 2.79,
activeAssetPath: Assets.images.alienBumper.b.active.keyName,
inactiveAssetPath: Assets.images.alienBumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.1),
),
);
final double _majorRadius;
final double _minorRadius;
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await _loadSprites();
deactivate();
await add(_spriteComponent);
}
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: _majorRadius,
minorRadius: _minorRadius,
)..rotate(15.9 * math.pi / 180);
final fixtureDef = FixtureDef(shape)
..friction = 0
..restitution = 4;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
Future<void> _loadSprites() async {
// TODO(alestiago): I think ideally we would like to do:
// Sprite(path).load so we don't require to store the activeAssetPath and
// the inactive assetPath.
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
}
/// Activates the [AlienBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
}
/// Deactivates the [AlienBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
}
}

@ -10,7 +10,7 @@ import 'package:pinball_components/pinball_components.dart';
/// {@endtemplate} /// {@endtemplate}
class Ball<T extends Forge2DGame> extends BodyComponent<T> class Ball<T extends Forge2DGame> extends BodyComponent<T>
with Layered, InitialPosition { with Layered, InitialPosition {
/// {@macro ball_body} /// {@macro ball}
Ball({ Ball({
required this.baseColor, required this.baseColor,
}) { }) {

@ -1,12 +1,8 @@
// ignore_for_file: comment_references
// TODO(alestiago): Revisit ignore lint rule once Kicker is moved to this
// package.
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// Indicates a side of the board. /// Indicates a side of the board.
/// ///
/// Usually used to position or mirror elements of a [PinballGame]; such as a /// Usually used to position or mirror elements of a pinball game; such as a
/// [Flipper] or [Kicker]. /// [Flipper] or [Kicker].
enum BoardSide { enum BoardSide {
/// The left side of the board. /// The left side of the board.

@ -1,3 +1,4 @@
export 'alien_bumper.dart';
export 'backboard.dart'; export 'backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
@ -6,17 +7,21 @@ export 'board_side.dart';
export 'boundaries.dart'; export 'boundaries.dart';
export 'camera_zoom.dart'; export 'camera_zoom.dart';
export 'chrome_dino.dart'; export 'chrome_dino.dart';
export 'dash_animatronic.dart';
export 'dash_nest_bumper.dart'; export 'dash_nest_bumper.dart';
export 'dino_walls.dart'; export 'dino_walls.dart';
export 'fire_effect.dart'; export 'fire_effect.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'flutter_sign_post.dart'; export 'flutter_sign_post.dart';
export 'google_letter.dart';
export 'initial_position.dart'; export 'initial_position.dart';
export 'joint_anchor.dart'; export 'joint_anchor.dart';
export 'kicker.dart'; export 'kicker.dart';
export 'launch_ramp.dart'; export 'launch_ramp.dart';
export 'layer.dart'; export 'layer.dart';
export 'plunger.dart';
export 'ramp_opening.dart'; export 'ramp_opening.dart';
export 'score_text.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship.dart'; export 'spaceship.dart';

@ -0,0 +1,53 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template dash_animatronic}
/// Animated Dash that sits on top of the [BigDashNestBumper].
/// {@endtemplate}
class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
/// {@macro dash_animatronic}
DashAnimatronic()
: super(
anchor: Anchor.center,
playing: false,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.dash.animatronic.keyName,
);
const amountPerRow = 12;
const amountPerColumn = 8;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
loop: false,
),
);
}
@override
void update(double dt) {
super.update(dt);
if (animation != null) {
if (animation!.isLastFrame) {
animation!.reset();
playing = false;
}
}
}
}

@ -63,8 +63,8 @@ class BigDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper} /// {@macro dash_nest_bumper}
BigDashNestBumper() BigDashNestBumper()
: super._( : super._(
activeAssetPath: Assets.images.dashBumper.main.active.keyName, activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dashBumper.main.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spriteComponent: SpriteComponent( spriteComponent: SpriteComponent(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -0.3), position: Vector2(0, -0.3),
@ -104,8 +104,8 @@ class SmallDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper} /// {@macro dash_nest_bumper}
SmallDashNestBumper.a() SmallDashNestBumper.a()
: this._( : this._(
activeAssetPath: Assets.images.dashBumper.a.active.keyName, activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dashBumper.a.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spriteComponent: SpriteComponent( spriteComponent: SpriteComponent(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0.35, -1.2), position: Vector2(0.35, -1.2),
@ -115,8 +115,8 @@ class SmallDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper} /// {@macro dash_nest_bumper}
SmallDashNestBumper.b() SmallDashNestBumper.b()
: this._( : this._(
activeAssetPath: Assets.images.dashBumper.b.active.keyName, activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dashBumper.b.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spriteComponent: SpriteComponent( spriteComponent: SpriteComponent(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0.35, -1.2), position: Vector2(0.35, -1.2),

@ -0,0 +1,95 @@
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template google_letter}
/// Circular sensor that represents a letter in "GOOGLE" for a given index.
/// {@endtemplate}
class GoogleLetter extends BodyComponent with InitialPosition {
/// {@macro google_letter}
GoogleLetter(int index)
: _sprite = _GoogleLetterSprite(
_GoogleLetterSprite.spritePaths[index],
);
final _GoogleLetterSprite _sprite;
/// Activates this [GoogleLetter].
// TODO(alestiago): Improve doc comment once activate and deactivate
// are implemented with the actual assets.
Future<void> activate() => _sprite.activate();
/// Deactivates this [GoogleLetter].
Future<void> deactivate() => _sprite.deactivate();
@override
Future<void> onLoad() async {
await super.onLoad();
await add(_sprite);
}
@override
Body createBody() {
final shape = CircleShape()..radius = 1.85;
final fixtureDef = FixtureDef(shape)..isSensor = true;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this
..type = BodyType.static;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _GoogleLetterSprite extends SpriteComponent with HasGameRef {
_GoogleLetterSprite(String path) : _path = path;
static final spritePaths = [
Assets.images.googleWord.letter1.keyName,
Assets.images.googleWord.letter2.keyName,
Assets.images.googleWord.letter3.keyName,
Assets.images.googleWord.letter4.keyName,
Assets.images.googleWord.letter5.keyName,
Assets.images.googleWord.letter6.keyName,
];
final String _path;
// TODO(alestiago): Correctly implement activate and deactivate once the
// assets are provided.
Future<void> activate() async {
await add(
_GoogleLetterColorEffect(color: Colors.green),
);
}
Future<void> deactivate() async {
await add(
_GoogleLetterColorEffect(color: Colors.red),
);
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(_path);
this.sprite = sprite;
// TODO(alestiago): Size correctly once the assets are provided.
size = sprite.originalSize / 5;
anchor = Anchor.center;
}
}
class _GoogleLetterColorEffect extends ColorEffect {
_GoogleLetterColorEffect({
required Color color,
}) : super(
color,
const Offset(0, 1),
EffectController(duration: 0.25),
);
}

@ -21,8 +21,7 @@ mixin Layered<T extends Forge2DGame> on BodyComponent<T> {
set layer(Layer value) { set layer(Layer value) {
_layer = value; _layer = value;
if (!isLoaded) { if (!isLoaded) {
// TODO(alestiago): Use loaded.whenComplete once provided. loaded.whenComplete(_applyMaskBits);
mounted.whenComplete(_applyMaskBits);
} else { } else {
_applyMaskBits(); _applyMaskBits();
} }

@ -1,16 +1,14 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template plunger} /// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the /// [Plunger] serves as a spring, that shoots the ball on the right side of the
/// playfield. /// playfield.
/// ///
/// [Plunger] ignores gravity so the player controls its downward [_pull]. /// [Plunger] ignores gravity so the player controls its downward [pull].
/// {@endtemplate} /// {@endtemplate}
class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { class Plunger extends BodyComponent with InitialPosition {
/// {@macro plunger} /// {@macro plunger}
Plunger({ Plunger({
required this.compressionDistance, required this.compressionDistance,
@ -43,7 +41,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
} }
/// Set a constant downward velocity on the [Plunger]. /// Set a constant downward velocity on the [Plunger].
void _pull() { void pull() {
body.linearVelocity = Vector2(0, -7); body.linearVelocity = Vector2(0, -7);
} }
@ -51,32 +49,11 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
/// ///
/// The velocity's magnitude depends on how far the [Plunger] has been pulled /// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition]. /// from its original [initialPosition].
void _release() { void release() {
final velocity = (initialPosition.y - body.position.y) * 5; final velocity = (initialPosition.y - body.position.y) * 5;
body.linearVelocity = Vector2(0, velocity); body.linearVelocity = Vector2(0, velocity);
} }
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
final keys = [
LogicalKeyboardKey.space,
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.keyS,
];
if (!keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
_pull();
} else if (event is RawKeyUpEvent) {
_release();
}
return false;
}
/// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical /// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical
/// motion. /// motion.
Future<void> _anchorToJoint() async { Future<void> _anchorToJoint() async {
@ -97,26 +74,24 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await _anchorToJoint(); await _anchorToJoint();
renderBody = false; renderBody = false;
await add(_PlungerSpriteComponent());
await _loadSprite();
} }
}
Future<void> _loadSprite() async { class _PlungerSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite( final sprite = await gameRef.loadSprite(
Assets.images.components.plunger.path, Assets.images.plunger.plunger.keyName,
); );
await add( this.sprite = sprite;
SpriteComponent( size = sprite.originalSize / 10;
sprite: sprite, anchor = Anchor.center;
size: Vector2(5.5, 40), position = Vector2(2, 19);
anchor: Anchor.center, angle = -0.033;
position: Vector2(2, 19),
angle: -0.033,
),
);
} }
} }
@ -133,14 +108,6 @@ class PlungerAnchor extends JointAnchor {
-plunger.compressionDistance, -plunger.compressionDistance,
); );
} }
@override
Body createBody() {
final bodyDef = BodyDef()
..position = initialPosition
..type = BodyType.static;
return world.createBody(bodyDef);
}
} }
/// {@template plunger_anchor_prismatic_joint_def} /// {@template plunger_anchor_prismatic_joint_def}

@ -0,0 +1,55 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template score_text}
/// A [TextComponent] that spawns at a given [position] with a moving animation.
/// {@endtemplate}
class ScoreText extends TextComponent {
/// {@macro score_text}
ScoreText({
required String text,
required Vector2 position,
this.color = Colors.black,
}) : super(
text: text,
position: position,
anchor: Anchor.center,
priority: 100,
);
late final Effect _effect;
/// The [text]'s [Color].
final Color color;
@override
Future<void> onLoad() async {
textRenderer = TextPaint(
style: TextStyle(
fontFamily: PinballFonts.pixeloidMono,
color: color,
fontSize: 4,
),
);
await add(
_effect = MoveEffect.by(
Vector2(0, -5),
EffectController(duration: 1),
),
);
}
@override
void update(double dt) {
super.update(dt);
if (_effect.controller.completed) {
removeFromParent();
}
}
}

@ -24,6 +24,16 @@ dev_dependencies:
flutter: flutter:
generate: true generate: true
fonts:
- family: PixeloidSans
fonts:
- asset: fonts/PixeloidSans-nR3g1.ttf
- asset: fonts/PixeloidSansBold-RpeJo.ttf
weight: 700
- family: PixeloidMono
fonts:
- asset: fonts/PixeloidMono-1G8ae.ttf
assets: assets:
- assets/images/ - assets/images/
- assets/images/baseboard/ - assets/images/baseboard/
@ -31,20 +41,25 @@ flutter:
- assets/images/dino/ - assets/images/dino/
- assets/images/flipper/ - assets/images/flipper/
- assets/images/launch_ramp/ - assets/images/launch_ramp/
- assets/images/dash_bumper/a/ - assets/images/dash/
- assets/images/dash_bumper/b/ - assets/images/dash/bumper/a/
- assets/images/dash_bumper/main/ - assets/images/dash/bumper/b/
- assets/images/dash/bumper/main/
- assets/images/spaceship/ - assets/images/spaceship/
- assets/images/spaceship/rail/ - assets/images/spaceship/rail/
- assets/images/spaceship/ramp/ - assets/images/spaceship/ramp/
- assets/images/chrome_dino/ - assets/images/chrome_dino/
- assets/images/kicker/ - assets/images/kicker/
- assets/images/plunger/
- assets/images/slingshot/ - assets/images/slingshot/
- assets/images/alien_bumper/a/
- assets/images/alien_bumper/b/
- assets/images/sparky/computer/ - assets/images/sparky/computer/
- assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/a/
- assets/images/sparky/bumper/b/ - assets/images/sparky/bumper/b/
- assets/images/sparky/bumper/c/ - assets/images/sparky/bumper/c/
- assets/images/backboard/ - assets/images/backboard/
- assets/images/google_word/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -23,10 +23,15 @@ void main() {
addChromeDinoStories(dashbook); addChromeDinoStories(dashbook);
addDashNestBumperStories(dashbook); addDashNestBumperStories(dashbook);
addKickerStories(dashbook); addKickerStories(dashbook);
addPlungerStories(dashbook);
addSlingshotStories(dashbook); addSlingshotStories(dashbook);
addSparkyBumperStories(dashbook); addSparkyBumperStories(dashbook);
addAlienZoneStories(dashbook);
addZoomStories(dashbook); addZoomStories(dashbook);
addBoundariesStories(dashbook); addBoundariesStories(dashbook);
addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook); addLaunchRampStories(dashbook);
addScoreTextStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -0,0 +1,28 @@
import 'dart:async';
import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AlienBumperAGame extends BasicBallGame {
AlienBumperAGame() : super(color: const Color(0xFF0000FF));
static const info = '''
Shows how a AlienBumperA is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperA = AlienBumper.a()
..initialPosition = Vector2(center.x - 20, center.y - 20)
..priority = 1;
await add(alienBumperA);
await traceAllBodies();
}
}

@ -0,0 +1,28 @@
import 'dart:async';
import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AlienBumperBGame extends BasicBallGame {
AlienBumperBGame() : super(color: const Color(0xFF0000FF));
static const info = '''
Shows how a AlienBumperB is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperB = AlienBumper.b()
..initialPosition = Vector2(center.x - 10, center.y + 10)
..priority = 1;
await add(alienBumperB);
await traceAllBodies();
}
}

@ -0,0 +1,25 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/alien_zone/alien_bumper_a_game.dart';
import 'package:sandbox/stories/alien_zone/alien_bumper_b_game.dart';
void addAlienZoneStories(Dashbook dashbook) {
dashbook.storiesOf('Alien Zone')
..add(
'Alien Bumper A',
(context) => GameWidget(
game: AlienBumperAGame()..trace = context.boolProperty('Trace', true),
),
codeLink: buildSourceLink('alien_zone/alien_bumper_a.dart'),
info: AlienBumperAGame.info,
)
..add(
'Alien Bumper B',
(context) => GameWidget(
game: AlienBumperBGame()..trace = context.boolProperty('Trace', true),
),
codeLink: buildSourceLink('alien_zone/alien_bumper_b.dart'),
info: AlienBumperAGame.info,
);
}

@ -7,7 +7,7 @@ class BallBoosterGame extends LineGame {
static const info = ''' static const info = '''
Shows how a Ball with a boost works. Shows how a Ball with a boost works.
Drag to launch a boosted Ball. - Drag to launch a boosted Ball.
'''; ''';
@override @override

@ -5,7 +5,7 @@ import 'package:sandbox/common/common.dart';
class BasicBallGame extends BasicGame with TapDetector, Traceable { class BasicBallGame extends BasicGame with TapDetector, Traceable {
BasicBallGame({ BasicBallGame({
required this.color, this.color = Colors.blue,
this.ballPriority = 0, this.ballPriority = 0,
this.ballLayer = Layer.all, this.ballLayer = Layer.all,
}); });
@ -13,7 +13,7 @@ class BasicBallGame extends BasicGame with TapDetector, Traceable {
static const info = ''' static const info = '''
Shows how a Ball works. Shows how a Ball works.
Tap anywhere on the screen to spawn a ball into the game. - Tap anywhere on the screen to spawn a ball into the game.
'''; ''';
final Color color; final Color color;

@ -4,8 +4,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class BoundariesGame extends BasicBallGame with Traceable { class BoundariesGame extends BasicBallGame with Traceable {
BoundariesGame() : super(color: const Color(0xFFFF0000));
static const info = ''' static const info = '''
Shows how Boundaries are rendered. Shows how Boundaries are rendered.

@ -6,7 +6,7 @@ class FireEffectGame extends LineGame {
static const info = ''' static const info = '''
Shows how the FireEffect renders. Shows how the FireEffect renders.
Drag a line to define the trail direction. - Drag a line to define the trail direction.
'''; ''';
@override @override

@ -7,8 +7,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class FlipperGame extends BasicBallGame with KeyboardEvents, Traceable { class FlipperGame extends BasicBallGame with KeyboardEvents, Traceable {
FlipperGame() : super(color: Colors.blue);
static const info = ''' static const info = '''
Shows how Flippers are rendered. Shows how Flippers are rendered.

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
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';
@ -7,8 +6,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class BigDashNestBumperGame extends BasicBallGame with Traceable { class BigDashNestBumperGame extends BasicBallGame with Traceable {
BigDashNestBumperGame() : super(color: const Color(0xFF0000FF));
static const info = ''' static const info = '''
Shows how a BigDashNestBumper is rendered. Shows how a BigDashNestBumper is rendered.

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
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';
@ -7,8 +6,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class FlutterSignPostGame extends BasicBallGame with Traceable { class FlutterSignPostGame extends BasicBallGame with Traceable {
FlutterSignPostGame() : super(color: const Color(0xFF0000FF));
static const info = ''' static const info = '''
Shows how a FlutterSignPost is rendered. Shows how a FlutterSignPost is rendered.

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
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';
@ -7,8 +6,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SmallDashNestBumperAGame extends BasicBallGame with Traceable { class SmallDashNestBumperAGame extends BasicBallGame with Traceable {
SmallDashNestBumperAGame() : super(color: const Color(0xFF0000FF));
static const info = ''' static const info = '''
Shows how a SmallDashNestBumper ("a") is rendered. Shows how a SmallDashNestBumper ("a") is rendered.

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
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';
@ -7,8 +6,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SmallDashNestBumperBGame extends BasicBallGame with Traceable { class SmallDashNestBumperBGame extends BasicBallGame with Traceable {
SmallDashNestBumperBGame() : super(color: const Color(0xFF0000FF));
static const info = ''' static const info = '''
Shows how a SmallDashNestBumper ("b") is rendered. Shows how a SmallDashNestBumper ("b") is rendered.

@ -0,0 +1,36 @@
import 'dart:ui';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class GoogleLetterGame extends BasicBallGame {
GoogleLetterGame() : super(color: const Color(0xFF009900));
static const info = '''
Shows how a GoogleLetter is rendered.
- Tap anywhere on the screen to spawn a ball into the game.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
addContactCallback(_BallGoogleLetterContactCallback());
camera.followVector2(Vector2.zero());
await add(GoogleLetter(0));
await traceAllBodies();
}
}
class _BallGoogleLetterContactCallback
extends ContactCallback<Ball, GoogleLetter> {
@override
void begin(Ball<Forge2DGame> a, GoogleLetter b, Contact contact) {
super.begin(a, b, contact);
b.activate();
}
}

@ -0,0 +1,15 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/google_word/google_letter_game.dart';
void addGoogleWordStories(Dashbook dashbook) {
dashbook.storiesOf('Google Word').add(
'Letter',
(context) => GameWidget(
game: GoogleLetterGame()..trace = context.boolProperty('Trace', true),
),
codeLink: buildSourceLink('google_word/letter.dart'),
info: GoogleLetterGame.info,
);
}

@ -4,8 +4,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class KickerGame extends BasicBallGame with Traceable { class KickerGame extends BasicBallGame with Traceable {
KickerGame() : super(color: const Color(0xFFFF0000));
static const info = ''' static const info = '''
Shows how Kickers are rendered. Shows how Kickers are rendered.

@ -10,7 +10,7 @@ class BasicLayerGame extends BasicGame with TapDetector {
static const info = ''' static const info = '''
Shows how Layers work when a Ball hits other components. Shows how Layers work when a Ball hits other components.
Tap anywhere on the screen to spawn a Ball into the game. - Tap anywhere on the screen to spawn a Ball into the game.
'''; ''';
final Color color; final Color color;

@ -0,0 +1,54 @@
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/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class PlungerGame extends BasicBallGame with KeyboardEvents, Traceable {
PlungerGame() : super(color: const Color(0xFFFF0000));
static const info = '''
Shows how Plunger is rendered.
- Activate the "trace" parameter to overlay the body.
- Tap anywhere on the screen to spawn a ball into the game.
''';
static const _downKeys = [
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
];
late Plunger plunger;
@override
Future<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y);
await add(plunger);
await traceAllBodies();
}
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
final movedPlungerDown = _downKeys.contains(event.logicalKey);
if (movedPlungerDown) {
if (event is RawKeyDownEvent) {
plunger.pull();
} else if (event is RawKeyUpEvent) {
plunger.release();
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
}

@ -0,0 +1,15 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/plunger/plunger_game.dart';
void addPlungerStories(Dashbook dashbook) {
dashbook.storiesOf('Plunger').add(
'Basic',
(context) => GameWidget(
game: PlungerGame()..trace = context.boolProperty('Trace', true),
),
codeLink: buildSourceLink('plunger_game/basic.dart'),
info: PlungerGame.info,
);
}

@ -0,0 +1,32 @@
import 'dart:math';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class ScoreTextBasicGame extends BasicGame with TapDetector {
static const info = '''
Simple game to show how score text works,
- Tap anywhere on the screen to spawn an text on the given location.
''';
final random = Random();
@override
Future<void> onLoad() async {
camera.followVector2(Vector2.zero());
}
@override
void onTapUp(TapUpInfo info) {
add(
ScoreText(
text: random.nextInt(100000).toString(),
color: Colors.white,
position: info.eventPosition.game..multiply(Vector2(1, -1)),
),
);
}
}

@ -0,0 +1,15 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/score_text/basic.dart';
void addScoreTextStories(Dashbook dashbook) {
dashbook.storiesOf('ScoreText').add(
'Basic',
(context) => GameWidget(
game: ScoreTextBasicGame(),
),
codeLink: buildSourceLink('score_text/basic.dart'),
info: ScoreTextBasicGame.info,
);
}

@ -4,8 +4,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SlingshotGame extends BasicBallGame with Traceable { class SlingshotGame extends BasicBallGame with Traceable {
SlingshotGame() : super(color: const Color(0xFFFF0000));
static const info = ''' static const info = '''
Shows how Slingshots are rendered. Shows how Slingshots are rendered.

@ -9,7 +9,7 @@ class BasicSpaceshipGame extends BasicGame with TapDetector {
static const info = ''' static const info = '''
Shows how a Spaceship works. Shows how a Spaceship works.
Tap anywhere on the screen to spawn a Ball into the game. - Tap anywhere on the screen to spawn a Ball into the game.
'''; ''';
@override @override

@ -6,12 +6,10 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SparkyBumperGame extends BasicBallGame with Traceable { class SparkyBumperGame extends BasicBallGame with Traceable {
SparkyBumperGame() : super(color: const Color(0xFF0000FF));
static const info = ''' static const info = '''
Shows how a SparkyBumper is rendered. Shows how a SparkyBumper is rendered.
Activate the "trace" parameter to overlay the body. - Activate the "trace" parameter to overlay the body.
'''; ''';
@override @override

@ -1,3 +1,4 @@
export 'alien_zone/stories.dart';
export 'ball/stories.dart'; export 'ball/stories.dart';
export 'baseboard/stories.dart'; export 'baseboard/stories.dart';
export 'boundaries/stories.dart'; export 'boundaries/stories.dart';
@ -5,8 +6,11 @@ export 'chrome_dino/stories.dart';
export 'effects/stories.dart'; export 'effects/stories.dart';
export 'flipper/stories.dart'; export 'flipper/stories.dart';
export 'flutter_forest/stories.dart'; export 'flutter_forest/stories.dart';
export 'google_word/stories.dart';
export 'launch_ramp/stories.dart'; export 'launch_ramp/stories.dart';
export 'layer/stories.dart'; export 'layer/stories.dart';
export 'plunger/stories.dart';
export 'score_text/stories.dart';
export 'slingshot/stories.dart'; export 'slingshot/stories.dart';
export 'spaceship/stories.dart'; export 'spaceship/stories.dart';
export 'spaceship_rail/stories.dart'; export 'spaceship_rail/stories.dart';

@ -5,8 +5,9 @@ import 'package:sandbox/common/common.dart';
class BasicCameraZoomGame extends BasicGame with TapDetector { class BasicCameraZoomGame extends BasicGame with TapDetector {
static const info = ''' static const info = '''
Simple game to demonstrate how the CameraZoom can be used. Shows how CameraZoom can be used.
Tap to zoom in/out
- Tap to zoom in/out.
'''; ''';
bool zoomApplied = false; bool zoomApplied = false;

@ -0,0 +1,68 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.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('AlienBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = AlienBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
flameTester.test('changes sprite', (game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
final spriteComponent = bumper.firstChild<SpriteComponent>()!;
final deactivatedSprite = spriteComponent.sprite;
bumper.activate();
expect(
spriteComponent.sprite,
isNot(equals(deactivatedSprite)),
);
final activatedSprite = spriteComponent.sprite;
bumper.deactivate();
expect(
spriteComponent.sprite,
isNot(equals(activatedSprite)),
);
expect(
activatedSprite,
isNot(equals(deactivatedSprite)),
);
});
});
}

@ -0,0 +1,38 @@
// ignore_for_file: cascade_invocations
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('DashAnimatronic', () {
flameTester.test(
'loads correctly',
(game) async {
final dashAnimatronic = DashAnimatronic();
await game.ensureAdd(dashAnimatronic);
expect(game.contains(dashAnimatronic), isTrue);
},
);
flameTester.test(
'stops animating after animation completes',
(game) async {
final dashAnimatronic = DashAnimatronic();
await game.ensureAdd(dashAnimatronic);
dashAnimatronic.playing = true;
dashAnimatronic.animation?.setToLast();
game.update(1);
expect(dashAnimatronic.playing, isFalse);
},
);
});
}

@ -0,0 +1,126 @@
// ignore_for_file: cascade_invocations
import 'package:flame/effects.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('Google Letter', () {
flameTester.test(
'0th loads correctly',
(game) async {
final googleLetter = GoogleLetter(0);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'1st loads correctly',
(game) async {
final googleLetter = GoogleLetter(1);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'2nd loads correctly',
(game) async {
final googleLetter = GoogleLetter(2);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'3d loads correctly',
(game) async {
final googleLetter = GoogleLetter(3);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'4th loads correctly',
(game) async {
final googleLetter = GoogleLetter(4);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'5th loads correctly',
(game) async {
final googleLetter = GoogleLetter(5);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
test('throws error when index out of range', () {
expect(() => GoogleLetter(-1), throwsA(isA<RangeError>()));
expect(() => GoogleLetter(6), throwsA(isA<RangeError>()));
});
group('activate', () {
flameTester.test('returns normally', (game) async {
final googleLetter = GoogleLetter(0);
await game.ensureAdd(googleLetter);
await expectLater(googleLetter.activate, returnsNormally);
});
flameTester.test('adds an Effect', (game) async {
final googleLetter = GoogleLetter(0);
await game.ensureAdd(googleLetter);
await googleLetter.activate();
await game.ready();
expect(
googleLetter.descendants().whereType<Effect>().length,
equals(1),
);
});
});
group('deactivate', () {
flameTester.test('returns normally', (game) async {
final googleLetter = GoogleLetter(0);
await game.ensureAdd(googleLetter);
await expectLater(googleLetter.deactivate, returnsNormally);
});
flameTester.test('adds an Effect', (game) async {
final googleLetter = GoogleLetter(0);
await game.ensureAdd(googleLetter);
await googleLetter.deactivate();
await game.ready();
expect(
googleLetter.descendants().whereType<Effect>().length,
equals(1),
);
});
});
});
}

@ -57,8 +57,6 @@ void main() {
const expectedLayer = Layer.spaceshipEntranceRamp; const expectedLayer = Layer.spaceshipEntranceRamp;
final component = TestLayeredBodyComponent()..layer = expectedLayer; final component = TestLayeredBodyComponent()..layer = expectedLayer;
await game.ensureAdd(component); await game.ensureAdd(component);
// TODO(alestiago): modify once component.loaded is available.
await component.mounted;
_expectLayerOnFixtures( _expectLayerOnFixtures(
fixtures: component.body.fixtures, fixtures: component.body.fixtures,
@ -79,8 +77,6 @@ void main() {
component.layer = expectedLayer; component.layer = expectedLayer;
await game.ensureAdd(component); await game.ensureAdd(component);
// TODO(alestiago): modify once component.loaded is available.
await component.mounted;
_expectLayerOnFixtures( _expectLayerOnFixtures(
fixtures: component.body.fixtures, fixtures: component.body.fixtures,

@ -1,12 +1,9 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:collection';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_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'; import '../../helpers/helpers.dart';
@ -117,13 +114,23 @@ void main() {
); );
}); });
group('onKeyEvent', () { group('pull', () {
final keys = UnmodifiableListView([ flameTester.test(
LogicalKeyboardKey.space, 'moves downwards when pull is called',
LogicalKeyboardKey.arrowDown, (game) async {
LogicalKeyboardKey.keyS, final plunger = Plunger(
]); compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
plunger.pull();
expect(plunger.body.linearVelocity.y, isNegative);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
group('release', () {
late Plunger plunger; late Plunger plunger;
setUp(() { setUp(() {
@ -132,57 +139,29 @@ void main() {
); );
}); });
testRawKeyUpEvents(keys, (event) {
final keyLabel = (event.logicalKey != LogicalKeyboardKey.space)
? event.logicalKey.keyLabel
: 'Space';
flameTester.test( flameTester.test(
'moves upwards when $keyLabel is released ' 'moves upwards when release is called '
'and plunger is below its starting position', 'and plunger is below its starting position', (game) async {
(game) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
plunger.body.setTransform(Vector2(0, -1), 0); plunger.body.setTransform(Vector2(0, -1), 0);
plunger.onKeyEvent(event, {}); plunger.release();
expect(plunger.body.linearVelocity.y, isPositive); expect(plunger.body.linearVelocity.y, isPositive);
expect(plunger.body.linearVelocity.x, isZero); expect(plunger.body.linearVelocity.x, isZero);
},
);
}); });
testRawKeyUpEvents(keys, (event) {
final keyLabel = (event.logicalKey != LogicalKeyboardKey.space)
? event.logicalKey.keyLabel
: 'Space';
flameTester.test( flameTester.test(
'does not move when $keyLabel is released ' 'does not move when release is called '
'and plunger is in its starting position', 'and plunger is in its starting position',
(game) async { (game) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
plunger.onKeyEvent(event, {}); plunger.release();
expect(plunger.body.linearVelocity.y, isZero); expect(plunger.body.linearVelocity.y, isZero);
expect(plunger.body.linearVelocity.x, isZero); expect(plunger.body.linearVelocity.x, isZero);
}, },
); );
}); });
testRawKeyDownEvents(keys, (event) {
final keyLabel = (event.logicalKey != LogicalKeyboardKey.space)
? event.logicalKey.keyLabel
: 'Space';
flameTester.test(
'moves downwards when $keyLabel is pressed',
(game) async {
await game.ensureAdd(plunger);
plunger.onKeyEvent(event, {});
expect(plunger.body.linearVelocity.y, isNegative);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
});
}); });
group('PlungerAnchor', () { group('PlungerAnchor', () {
@ -210,11 +189,13 @@ void main() {
group('PlungerAnchorPrismaticJointDef', () { group('PlungerAnchorPrismaticJointDef', () {
const compressionDistance = 10.0; const compressionDistance = 10.0;
late Plunger plunger; late Plunger plunger;
late PlungerAnchor anchor;
setUp(() { setUp(() {
plunger = Plunger( plunger = Plunger(
compressionDistance: compressionDistance, compressionDistance: compressionDistance,
); );
anchor = PlungerAnchor(plunger: plunger);
}); });
group('initializes with', () { group('initializes with', () {
@ -222,7 +203,6 @@ void main() {
'plunger body as bodyA', 'plunger body as bodyA',
(game) async { (game) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
@ -238,7 +218,6 @@ void main() {
'anchor body as bodyB', 'anchor body as bodyB',
(game) async { (game) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
@ -255,7 +234,6 @@ void main() {
'limits enabled', 'limits enabled',
(game) async { (game) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
@ -272,7 +250,6 @@ void main() {
'lower translation limit as negative infinity', 'lower translation limit as negative infinity',
(game) async { (game) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
@ -289,7 +266,6 @@ void main() {
'connected body collison enabled', 'connected body collison enabled',
(game) async { (game) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
@ -303,8 +279,6 @@ void main() {
); );
}); });
testRawKeyUpEvents([LogicalKeyboardKey.space], (event) {
late final anchor = PlungerAnchor(plunger: plunger);
flameTester.testGameWidget( flameTester.testGameWidget(
'plunger cannot go below anchor', 'plunger cannot go below anchor',
setUp: (game, tester) async { setUp: (game, tester) async {
@ -326,14 +300,11 @@ void main() {
expect(plunger.body.position.y > anchor.body.position.y, isTrue); expect(plunger.body.position.y > anchor.body.position.y, isTrue);
}, },
); );
});
testRawKeyUpEvents([LogicalKeyboardKey.space], (event) {
flameTester.testGameWidget( flameTester.testGameWidget(
'plunger cannot excessively exceed starting position', 'plunger cannot excessively exceed starting position',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
@ -351,5 +322,4 @@ void main() {
}, },
); );
}); });
});
} }

@ -0,0 +1,75 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('ScoreText', () {
final flameTester = FlameTester(TestGame.new);
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreText(
text: '123',
position: Vector2.zero(),
color: Colors.white,
),
);
},
verify: (game, tester) async {
final texts = game.descendants().whereType<TextComponent>().length;
expect(texts, equals(1));
},
);
flameTester.testGameWidget(
'has a movement effect',
setUp: (game, tester) async {
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreText(
text: '123',
position: Vector2.zero(),
color: Colors.white,
),
);
game.update(0.5);
await tester.pump();
},
verify: (game, tester) async {
final text = game.descendants().whereType<TextComponent>().first;
expect(text.firstChild<MoveEffect>(), isNotNull);
},
);
flameTester.testGameWidget(
'is removed once finished',
setUp: (game, tester) async {
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreText(
text: '123',
position: Vector2.zero(),
color: Colors.white,
),
);
game.update(1);
game.update(0); // Ensure all component removals
},
verify: (game, tester) async {
expect(game.children.length, equals(0));
},
);
});
}

@ -0,0 +1,115 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
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/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('AlienZone', () {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final alienZone = AlienZone();
await game.ensureAdd(alienZone);
expect(game.contains(alienZone), isTrue);
},
);
group('loads', () {
flameTester.test(
'two AlienBumper',
(game) async {
await game.ready();
final alienZone = AlienZone();
await game.ensureAdd(alienZone);
expect(
alienZone.descendants().whereType<AlienBumper>().length,
equals(2),
);
},
);
});
group('bumpers', () {
late ControlledAlienBumper controlledAlienBumper;
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
);
flameTester.testGameWidget(
'activate when deactivated bumper is hit',
setUp: (game, tester) async {
controlledAlienBumper = ControlledAlienBumper.a();
await game.ensureAdd(controlledAlienBumper);
controlledAlienBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledAlienBumper.controller.isActivated, isTrue);
},
);
flameTester.testGameWidget(
'deactivate when activated bumper is hit',
setUp: (game, tester) async {
controlledAlienBumper = ControlledAlienBumper.a();
await game.ensureAdd(controlledAlienBumper);
controlledAlienBumper.controller.hit();
controlledAlienBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledAlienBumper.controller.isActivated, isFalse);
},
);
flameBlocTester.testGameWidget(
'add Scored event',
setUp: (game, tester) async {
final ball = Ball(baseColor: const Color(0xFF00FFFF));
final alienZone = AlienZone();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ensureAdd(alienZone);
await game.ensureAdd(ball);
game.addContactCallback(BallScorePointsCallback(game));
final bumpers = alienZone.descendants().whereType<ScorePoints>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
verify(
() => gameBloc.add(
Scored(points: bumper.points),
),
).called(1);
}
},
);
});
});
}

@ -0,0 +1,78 @@
import 'dart:collection';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.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 flameTester = FlameTester(EmptyPinballGameTest.new);
group('PlungerController', () {
group('onKeyEvent', () {
final downKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyS,
]);
late Plunger plunger;
late PlungerController controller;
setUp(() {
plunger = Plunger(compressionDistance: 10);
controller = PlungerController(plunger);
plunger.add(controller);
});
testRawKeyDownEvents(downKeys, (event) {
flameTester.test(
'moves down '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
await game.ensureAdd(plunger);
controller.onKeyEvent(event, {});
expect(plunger.body.linearVelocity.y, isNegative);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(downKeys, (event) {
flameTester.test(
'moves up '
'when ${event.logicalKey.keyLabel} is released '
'and plunger is below its starting position',
(game) async {
await game.ensureAdd(plunger);
plunger.body.setTransform(Vector2(0, -1), 0);
controller.onKeyEvent(event, {});
expect(plunger.body.linearVelocity.y, isPositive);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(downKeys, (event) {
flameTester.test(
'does not move when ${event.logicalKey.keyLabel} is released '
'and plunger is in its starting position',
(game) async {
await game.ensureAdd(plunger);
controller.onKeyEvent(event, {});
expect(plunger.body.linearVelocity.y, isZero);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
});
});
}

@ -18,7 +18,6 @@ void main() {
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
await game.ready();
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
@ -30,7 +29,6 @@ void main() {
flameTester.test( flameTester.test(
'a FlutterSignPost', 'a FlutterSignPost',
(game) async { (game) async {
await game.ready();
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
@ -41,10 +39,22 @@ void main() {
}, },
); );
flameTester.test(
'a DashAnimatronic',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.firstChild<DashAnimatronic>(),
isNotNull,
);
},
);
flameTester.test( flameTester.test(
'a BigDashNestBumper', 'a BigDashNestBumper',
(game) async { (game) async {
await game.ready();
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
@ -58,7 +68,6 @@ void main() {
flameTester.test( flameTester.test(
'two SmallDashNestBumper', 'two SmallDashNestBumper',
(game) async { (game) async {
await game.ready();
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
@ -102,14 +111,15 @@ void main() {
}); });
flameTester.test( flameTester.test(
'onNewState adds a new ball', 'onNewState adds a new ball after a duration',
(game) async { (game) async {
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
await game.ready();
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
final previousBalls = game.descendants().whereType<Ball>().length; final previousBalls = game.descendants().whereType<Ball>().length;
flutterForest.controller.onNewState(MockGameState()); flutterForest.controller.onNewState(MockGameState());
await Future<void>.delayed(const Duration(milliseconds: 700));
await game.ready(); await game.ready();
expect( expect(
@ -119,6 +129,20 @@ void main() {
}, },
); );
flameTester.test(
'onNewState starts Dash animatronic',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
flutterForest.controller.onNewState(MockGameState());
final dashAnimatronic =
game.descendants().whereType<DashAnimatronic>().single;
expect(dashAnimatronic.playing, isTrue);
},
);
group('bumpers', () { group('bumpers', () {
late Ball ball; late Ball ball;
late GameBloc gameBloc; late GameBloc gameBloc;

@ -0,0 +1,115 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('ScoreEffectController', () {
late ScoreEffectController controller;
late PinballGame game;
setUpAll(() {
registerFallbackValue(Component());
});
setUp(() {
game = MockPinballGame();
when(() => game.add(any())).thenAnswer((_) async {});
controller = ScoreEffectController(game);
});
group('listenWhen', () {
test('returns true when the user has earned points', () {
const previous = GameState.initial();
const current = GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
);
expect(controller.listenWhen(previous, current), isTrue);
});
test(
'returns true when the user has earned points and there was no '
'previous state',
() {
const current = GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
);
expect(controller.listenWhen(null, current), isTrue);
},
);
test(
'returns false when no points were earned',
() {
const current = GameState.initial();
const previous = GameState.initial();
expect(controller.listenWhen(previous, current), isFalse);
},
);
});
group('onNewState', () {
test(
'adds a ScoreText with the correct score for the '
'first time',
() {
const state = GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
);
controller.onNewState(state);
final effect =
verify(() => game.add(captureAny())).captured.first as ScoreText;
expect(effect.text, equals('10'));
},
);
test('adds a ScoreTextEffect with the correct score', () {
controller.onNewState(
const GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
),
);
controller.onNewState(
const GameState(
score: 14,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
),
);
final effect =
verify(() => game.add(captureAny())).captured.last as ScoreText;
expect(effect.text, equals('4'));
});
});
});
}

@ -70,6 +70,14 @@ void main() {
}, },
); );
flameTester.test(
'one AlienZone',
(game) async {
await game.ready();
expect(game.children.whereType<AlienZone>().length, equals(1));
},
);
group('controller', () { group('controller', () {
// TODO(alestiago): Write test to be controller agnostic. // TODO(alestiago): Write test to be controller agnostic.
group('listenWhen', () { group('listenWhen', () {

Loading…
Cancel
Save