diff --git a/lib/game/components/alien_zone.dart b/lib/game/components/alien_zone.dart new file mode 100644 index 00000000..3d8b75ae --- /dev/null +++ b/lib/game/components/alien_zone.dart @@ -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 { + /// {@macro alien_zone} + AlienZone(); + + @override + Future 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 + with HasGameRef { + /// {@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, Ball> { + @override + void begin( + Controls<_AlienBumperController> controlledAlienBumper, + Ball _, + Contact __, + ) { + controlledAlienBumper.controller.hit(); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 31d3b917..6a9d6a3d 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,12 +1,14 @@ +export 'alien_zone.dart'; export 'board.dart'; export 'bonus_word.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; +export 'controlled_plunger.dart'; export 'controlled_sparky_computer.dart'; export 'flutter_forest.dart'; export 'game_flow_controller.dart'; -export 'plunger.dart'; +export 'score_effect_controller.dart'; export 'score_points.dart'; export 'sparky_fire_zone.dart'; export 'wall.dart'; diff --git a/lib/game/components/controlled_plunger.dart b/lib/game/components/controlled_plunger.dart new file mode 100644 index 00000000..167f129e --- /dev/null +++ b/lib/game/components/controlled_plunger.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 { + /// {@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 + 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 _keys = [ + LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.space, + LogicalKeyboardKey.keyS, + ]; + + @override + bool onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (!_keys.contains(event.logicalKey)) return true; + + if (event is RawKeyDownEvent) { + component.pull(); + } else if (event is RawKeyUpEvent) { + component.release(); + } + + return false; + } +} diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart index 966b0a6b..76699ec9 100644 --- a/lib/game/components/flutter_forest.dart +++ b/lib/game/components/flutter_forest.dart @@ -37,12 +37,14 @@ class FlutterForest extends Component with Controls<_FlutterForestController> { final smallRightNest = _ControlledSmallDashNestBumper.b( id: 'small_nest_bumper_b', )..initialPosition = Vector2(23.3, 46.75); + final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66); await addAll([ signPost, smallLeftNest, smallRightNest, bigNest, + dashAnimatronic, ]); } } @@ -68,7 +70,13 @@ class _FlutterForestController extends ComponentController void onNewState(GameState state) { super.onNewState(state); - gameRef.add( + component.firstChild()?.playing = true; + _addBonusBall(); + } + + Future _addBonusBall() async { + await Future.delayed(const Duration(milliseconds: 700)); + await gameRef.add( ControlledBall.bonus(theme: gameRef.theme) ..initialPosition = Vector2(17.2, 52.7), ); diff --git a/lib/game/components/score_effect_controller.dart b/lib/game/components/score_effect_controller.dart new file mode 100644 index 00000000..7fafd4b5 --- /dev/null +++ b/lib/game/components/score_effect_controller.dart @@ -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 + with BlocComponent { + /// {@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), + ), + ), + ); + } +} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 54ccc6eb..cce1e658 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -25,12 +25,13 @@ extension PinballGameAssetsX on PinballGame { ), images.load(components.Assets.images.dino.dinoLandTop.keyName), images.load(components.Assets.images.dino.dinoLandBottom.keyName), - images.load(components.Assets.images.dashBumper.a.active.keyName), - images.load(components.Assets.images.dashBumper.a.inactive.keyName), - images.load(components.Assets.images.dashBumper.b.active.keyName), - images.load(components.Assets.images.dashBumper.b.inactive.keyName), - images.load(components.Assets.images.dashBumper.main.active.keyName), - images.load(components.Assets.images.dashBumper.main.inactive.keyName), + images.load(components.Assets.images.dash.animatronic.keyName), + images.load(components.Assets.images.dash.bumper.a.active.keyName), + images.load(components.Assets.images.dash.bumper.a.inactive.keyName), + images.load(components.Assets.images.dash.bumper.b.active.keyName), + images.load(components.Assets.images.dash.bumper.b.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.outer.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.chromeDino.mouth.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.top.keyName), images.load(components.Assets.images.sparky.bumper.a.active.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 27a56743..cad2eac5 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -38,6 +38,7 @@ class PinballGame extends Forge2DGame Future onLoad() async { _addContactCallbacks(); + unawaited(add(ScoreEffectController(this))); unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(CameraController(this))); unawaited(add(Backboard(position: Vector2(0, -88)))); @@ -47,11 +48,12 @@ class PinballGame extends Forge2DGame unawaited(addFromBlueprint(LaunchRamp())); unawaited(addFromBlueprint(ControlledSparkyComputer())); - final plunger = Plunger(compressionDistance: 29) + final plunger = ControlledPlunger(compressionDistance: 29) ..initialPosition = Vector2(38, -19); await add(plunger); unawaited(add(Board())); + unawaited(add(AlienZone())); unawaited(add(SparkyFireZone())); unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(DinoWalls())); diff --git a/packages/pinball_components/assets/images/alien_bumper/a/active.png b/packages/pinball_components/assets/images/alien_bumper/a/active.png new file mode 100644 index 00000000..92943dfc Binary files /dev/null and b/packages/pinball_components/assets/images/alien_bumper/a/active.png differ diff --git a/packages/pinball_components/assets/images/alien_bumper/a/inactive.png b/packages/pinball_components/assets/images/alien_bumper/a/inactive.png new file mode 100644 index 00000000..99745fcf Binary files /dev/null and b/packages/pinball_components/assets/images/alien_bumper/a/inactive.png differ diff --git a/packages/pinball_components/assets/images/alien_bumper/b/active.png b/packages/pinball_components/assets/images/alien_bumper/b/active.png new file mode 100644 index 00000000..ec7dc0cb Binary files /dev/null and b/packages/pinball_components/assets/images/alien_bumper/b/active.png differ diff --git a/packages/pinball_components/assets/images/alien_bumper/b/inactive.png b/packages/pinball_components/assets/images/alien_bumper/b/inactive.png new file mode 100644 index 00000000..23f7f4b7 Binary files /dev/null and b/packages/pinball_components/assets/images/alien_bumper/b/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash/animatronic.png b/packages/pinball_components/assets/images/dash/animatronic.png new file mode 100644 index 00000000..e67cafef Binary files /dev/null and b/packages/pinball_components/assets/images/dash/animatronic.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/a/active.png b/packages/pinball_components/assets/images/dash/bumper/a/active.png similarity index 100% rename from packages/pinball_components/assets/images/dash_bumper/a/active.png rename to packages/pinball_components/assets/images/dash/bumper/a/active.png diff --git a/packages/pinball_components/assets/images/dash_bumper/a/inactive.png b/packages/pinball_components/assets/images/dash/bumper/a/inactive.png similarity index 100% rename from packages/pinball_components/assets/images/dash_bumper/a/inactive.png rename to packages/pinball_components/assets/images/dash/bumper/a/inactive.png diff --git a/packages/pinball_components/assets/images/dash_bumper/b/active.png b/packages/pinball_components/assets/images/dash/bumper/b/active.png similarity index 100% rename from packages/pinball_components/assets/images/dash_bumper/b/active.png rename to packages/pinball_components/assets/images/dash/bumper/b/active.png diff --git a/packages/pinball_components/assets/images/dash_bumper/b/inactive.png b/packages/pinball_components/assets/images/dash/bumper/b/inactive.png similarity index 100% rename from packages/pinball_components/assets/images/dash_bumper/b/inactive.png rename to packages/pinball_components/assets/images/dash/bumper/b/inactive.png diff --git a/packages/pinball_components/assets/images/dash_bumper/main/active.png b/packages/pinball_components/assets/images/dash/bumper/main/active.png similarity index 100% rename from packages/pinball_components/assets/images/dash_bumper/main/active.png rename to packages/pinball_components/assets/images/dash/bumper/main/active.png diff --git a/packages/pinball_components/assets/images/dash_bumper/main/inactive.png b/packages/pinball_components/assets/images/dash/bumper/main/inactive.png similarity index 100% rename from packages/pinball_components/assets/images/dash_bumper/main/inactive.png rename to packages/pinball_components/assets/images/dash/bumper/main/inactive.png diff --git a/packages/pinball_components/assets/images/google_word/letter1.png b/packages/pinball_components/assets/images/google_word/letter1.png new file mode 100644 index 00000000..f79ea687 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter1.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter2.png b/packages/pinball_components/assets/images/google_word/letter2.png new file mode 100644 index 00000000..e9d205e3 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter2.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter3.png b/packages/pinball_components/assets/images/google_word/letter3.png new file mode 100644 index 00000000..e9d205e3 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter3.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter4.png b/packages/pinball_components/assets/images/google_word/letter4.png new file mode 100644 index 00000000..f79ea687 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter4.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter5.png b/packages/pinball_components/assets/images/google_word/letter5.png new file mode 100644 index 00000000..13f30fb7 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter5.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter6.png b/packages/pinball_components/assets/images/google_word/letter6.png new file mode 100644 index 00000000..7d87654b Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter6.png differ diff --git a/packages/pinball_components/assets/images/plunger/plunger.png b/packages/pinball_components/assets/images/plunger/plunger.png new file mode 100644 index 00000000..f3cbdf0f Binary files /dev/null and b/packages/pinball_components/assets/images/plunger/plunger.png differ diff --git a/packages/pinball_components/fonts/PixeloidMono-1G8ae.ttf b/packages/pinball_components/fonts/PixeloidMono-1G8ae.ttf new file mode 100644 index 00000000..a797c1e1 Binary files /dev/null and b/packages/pinball_components/fonts/PixeloidMono-1G8ae.ttf differ diff --git a/packages/pinball_components/fonts/PixeloidSans-nR3g1.ttf b/packages/pinball_components/fonts/PixeloidSans-nR3g1.ttf new file mode 100644 index 00000000..2f9a03b4 Binary files /dev/null and b/packages/pinball_components/fonts/PixeloidSans-nR3g1.ttf differ diff --git a/packages/pinball_components/fonts/PixeloidSansBold-RpeJo.ttf b/packages/pinball_components/fonts/PixeloidSansBold-RpeJo.ttf new file mode 100644 index 00000000..81194f5d Binary files /dev/null and b/packages/pinball_components/fonts/PixeloidSansBold-RpeJo.ttf differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index bc04349a..1a4c9861 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -3,36 +3,58 @@ /// FlutterGen /// ***************************************************** +// ignore_for_file: directives_ordering,unnecessary_import + import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); + $AssetsImagesAlienBumperGen get alienBumper => + const $AssetsImagesAlienBumperGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); + + /// File path: assets/images/ball.png AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); + $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesChromeDinoGen get chromeDino => const $AssetsImagesChromeDinoGen(); - $AssetsImagesDashBumperGen get dashBumper => - const $AssetsImagesDashBumperGen(); + $AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); + + /// File path: assets/images/flutter_sign_post.png AssetGenImage get flutterSignPost => const AssetGenImage('assets/images/flutter_sign_post.png'); + + $AssetsImagesGoogleWordGen get googleWord => + const $AssetsImagesGoogleWordGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); + $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); } +class $AssetsImagesAlienBumperGen { + const $AssetsImagesAlienBumperGen(); + + $AssetsImagesAlienBumperAGen get a => const $AssetsImagesAlienBumperAGen(); + $AssetsImagesAlienBumperBGen get b => const $AssetsImagesAlienBumperBGen(); +} + class $AssetsImagesBackboardGen { const $AssetsImagesBackboardGen(); + /// File path: assets/images/backboard/backboard_game_over.png AssetGenImage get backboardGameOver => const AssetGenImage('assets/images/backboard/backboard_game_over.png'); + + /// File path: assets/images/backboard/backboard_scores.png AssetGenImage get backboardScores => const AssetGenImage('assets/images/backboard/backboard_scores.png'); } @@ -40,8 +62,11 @@ class $AssetsImagesBackboardGen { class $AssetsImagesBaseboardGen { const $AssetsImagesBaseboardGen(); + /// File path: assets/images/baseboard/left.png AssetGenImage get left => const AssetGenImage('assets/images/baseboard/left.png'); + + /// File path: assets/images/baseboard/right.png AssetGenImage get right => const AssetGenImage('assets/images/baseboard/right.png'); } @@ -49,8 +74,11 @@ class $AssetsImagesBaseboardGen { class $AssetsImagesBoundaryGen { const $AssetsImagesBoundaryGen(); + /// File path: assets/images/boundary/bottom.png AssetGenImage get bottom => const AssetGenImage('assets/images/boundary/bottom.png'); + + /// File path: assets/images/boundary/outer.png AssetGenImage get outer => const AssetGenImage('assets/images/boundary/outer.png'); } @@ -58,26 +86,33 @@ class $AssetsImagesBoundaryGen { class $AssetsImagesChromeDinoGen { const $AssetsImagesChromeDinoGen(); + /// File path: assets/images/chrome_dino/head.png AssetGenImage get head => const AssetGenImage('assets/images/chrome_dino/head.png'); + + /// File path: assets/images/chrome_dino/mouth.png AssetGenImage get mouth => const AssetGenImage('assets/images/chrome_dino/mouth.png'); } -class $AssetsImagesDashBumperGen { - const $AssetsImagesDashBumperGen(); +class $AssetsImagesDashGen { + const $AssetsImagesDashGen(); - $AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen(); - $AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen(); - $AssetsImagesDashBumperMainGen get main => - const $AssetsImagesDashBumperMainGen(); + /// File path: assets/images/dash/animatronic.png + AssetGenImage get animatronic => + const AssetGenImage('assets/images/dash/animatronic.png'); + + $AssetsImagesDashBumperGen get bumper => const $AssetsImagesDashBumperGen(); } class $AssetsImagesDinoGen { const $AssetsImagesDinoGen(); + /// File path: assets/images/dino/dino-land-bottom.png AssetGenImage get dinoLandBottom => const AssetGenImage('assets/images/dino/dino-land-bottom.png'); + + /// File path: assets/images/dino/dino-land-top.png AssetGenImage get dinoLandTop => const AssetGenImage('assets/images/dino/dino-land-top.png'); } @@ -85,17 +120,51 @@ class $AssetsImagesDinoGen { class $AssetsImagesFlipperGen { const $AssetsImagesFlipperGen(); + /// File path: assets/images/flipper/left.png AssetGenImage get left => const AssetGenImage('assets/images/flipper/left.png'); + + /// File path: assets/images/flipper/right.png AssetGenImage get right => 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 { const $AssetsImagesKickerGen(); + /// File path: assets/images/kicker/left.png AssetGenImage get left => const AssetGenImage('assets/images/kicker/left.png'); + + /// File path: assets/images/kicker/right.png AssetGenImage get right => const AssetGenImage('assets/images/kicker/right.png'); } @@ -103,21 +172,39 @@ class $AssetsImagesKickerGen { class $AssetsImagesLaunchRampGen { const $AssetsImagesLaunchRampGen(); + /// File path: assets/images/launch_ramp/foreground-railing.png AssetGenImage get foregroundRailing => const AssetGenImage('assets/images/launch_ramp/foreground-railing.png'); + + /// File path: assets/images/launch_ramp/ramp.png AssetGenImage get ramp => 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 { const $AssetsImagesSlingshotGen(); + /// File path: assets/images/slingshot/left_lower.png AssetGenImage get leftLower => const AssetGenImage('assets/images/slingshot/left_lower.png'); + + /// File path: assets/images/slingshot/left_upper.png AssetGenImage get leftUpper => const AssetGenImage('assets/images/slingshot/left_upper.png'); + + /// File path: assets/images/slingshot/right_lower.png AssetGenImage get rightLower => const AssetGenImage('assets/images/slingshot/right_lower.png'); + + /// File path: assets/images/slingshot/right_upper.png AssetGenImage get rightUpper => const AssetGenImage('assets/images/slingshot/right_upper.png'); } @@ -125,12 +212,16 @@ class $AssetsImagesSlingshotGen { class $AssetsImagesSpaceshipGen { const $AssetsImagesSpaceshipGen(); + /// File path: assets/images/spaceship/bridge.png AssetGenImage get bridge => const AssetGenImage('assets/images/spaceship/bridge.png'); + $AssetsImagesSpaceshipRailGen get rail => const $AssetsImagesSpaceshipRailGen(); $AssetsImagesSpaceshipRampGen get ramp => const $AssetsImagesSpaceshipRampGen(); + + /// File path: assets/images/spaceship/saucer.png AssetGenImage get saucer => const AssetGenImage('assets/images/spaceship/saucer.png'); } @@ -144,38 +235,47 @@ class $AssetsImagesSparkyGen { const $AssetsImagesSparkyComputerGen(); } -class $AssetsImagesDashBumperAGen { - const $AssetsImagesDashBumperAGen(); +class $AssetsImagesAlienBumperAGen { + const $AssetsImagesAlienBumperAGen(); + /// File path: assets/images/alien_bumper/a/active.png 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 => - const AssetGenImage('assets/images/dash_bumper/a/inactive.png'); + const AssetGenImage('assets/images/alien_bumper/a/inactive.png'); } -class $AssetsImagesDashBumperBGen { - const $AssetsImagesDashBumperBGen(); +class $AssetsImagesAlienBumperBGen { + const $AssetsImagesAlienBumperBGen(); + /// File path: assets/images/alien_bumper/b/active.png 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 => - const AssetGenImage('assets/images/dash_bumper/b/inactive.png'); + const AssetGenImage('assets/images/alien_bumper/b/inactive.png'); } -class $AssetsImagesDashBumperMainGen { - const $AssetsImagesDashBumperMainGen(); +class $AssetsImagesDashBumperGen { + const $AssetsImagesDashBumperGen(); - AssetGenImage get active => - const AssetGenImage('assets/images/dash_bumper/main/active.png'); - AssetGenImage get inactive => - const AssetGenImage('assets/images/dash_bumper/main/inactive.png'); + $AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen(); + $AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen(); + $AssetsImagesDashBumperMainGen get main => + const $AssetsImagesDashBumperMainGen(); } class $AssetsImagesSpaceshipRailGen { const $AssetsImagesSpaceshipRailGen(); + /// File path: assets/images/spaceship/rail/foreground.png AssetGenImage get foreground => const AssetGenImage('assets/images/spaceship/rail/foreground.png'); + + /// File path: assets/images/spaceship/rail/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/rail/main.png'); } @@ -183,12 +283,19 @@ class $AssetsImagesSpaceshipRailGen { class $AssetsImagesSpaceshipRampGen { const $AssetsImagesSpaceshipRampGen(); + /// File path: assets/images/spaceship/ramp/board-opening.png AssetGenImage get boardOpening => const AssetGenImage('assets/images/spaceship/ramp/board-opening.png'); + + /// File path: assets/images/spaceship/ramp/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/ramp/main.png'); + + /// File path: assets/images/spaceship/ramp/railing-background.png AssetGenImage get railingBackground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-background.png'); + + /// File path: assets/images/spaceship/ramp/railing-foreground.png AssetGenImage get railingForeground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-foreground.png'); } @@ -213,6 +320,42 @@ class $AssetsImagesSparkyComputerGen { 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 { const $AssetsImagesSparkyBumperAGen(); diff --git a/packages/pinball_components/lib/gen/fonts.gen.dart b/packages/pinball_components/lib/gen/fonts.gen.dart new file mode 100644 index 00000000..5f77da16 --- /dev/null +++ b/packages/pinball_components/lib/gen/fonts.gen.dart @@ -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'; +} diff --git a/packages/pinball_components/lib/gen/pinball_fonts.dart b/packages/pinball_components/lib/gen/pinball_fonts.dart new file mode 100644 index 00000000..c1b3c6fa --- /dev/null +++ b/packages/pinball_components/lib/gen/pinball_fonts.dart @@ -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); +} diff --git a/packages/pinball_components/lib/pinball_components.dart b/packages/pinball_components/lib/pinball_components.dart index b00b9d5b..2551b54e 100644 --- a/packages/pinball_components/lib/pinball_components.dart +++ b/packages/pinball_components/lib/pinball_components.dart @@ -1,4 +1,5 @@ library pinball_components; export 'gen/assets.gen.dart'; +export 'gen/pinball_fonts.dart'; export 'src/pinball_components.dart'; diff --git a/packages/pinball_components/lib/src/components/alien_bumper.dart b/packages/pinball_components/lib/src/components/alien_bumper.dart new file mode 100644 index 00000000..75b0560d --- /dev/null +++ b/packages/pinball_components/lib/src/components/alien_bumper.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 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 _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; + } +} diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index c32b8b18..1261791d 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -10,7 +10,7 @@ import 'package:pinball_components/pinball_components.dart'; /// {@endtemplate} class Ball extends BodyComponent with Layered, InitialPosition { - /// {@macro ball_body} + /// {@macro ball} Ball({ required this.baseColor, }) { diff --git a/packages/pinball_components/lib/src/components/board_side.dart b/packages/pinball_components/lib/src/components/board_side.dart index ac530567..9f6be71c 100644 --- a/packages/pinball_components/lib/src/components/board_side.dart +++ b/packages/pinball_components/lib/src/components/board_side.dart @@ -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'; /// 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]. enum BoardSide { /// The left side of the board. diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index b4ba70e2..acb8a7c5 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,3 +1,4 @@ +export 'alien_bumper.dart'; export 'backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; @@ -6,17 +7,21 @@ export 'board_side.dart'; export 'boundaries.dart'; export 'camera_zoom.dart'; export 'chrome_dino.dart'; +export 'dash_animatronic.dart'; export 'dash_nest_bumper.dart'; export 'dino_walls.dart'; export 'fire_effect.dart'; export 'flipper.dart'; export 'flutter_sign_post.dart'; +export 'google_letter.dart'; export 'initial_position.dart'; export 'joint_anchor.dart'; export 'kicker.dart'; export 'launch_ramp.dart'; export 'layer.dart'; +export 'plunger.dart'; export 'ramp_opening.dart'; +export 'score_text.dart'; export 'shapes/shapes.dart'; export 'slingshot.dart'; export 'spaceship.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_animatronic.dart b/packages/pinball_components/lib/src/components/dash_animatronic.dart new file mode 100644 index 00000000..1ab7e76e --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_animatronic.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 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; + } + } + } +} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper.dart index 447b4156..cec5b42a 100644 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper.dart @@ -63,8 +63,8 @@ class BigDashNestBumper extends DashNestBumper { /// {@macro dash_nest_bumper} BigDashNestBumper() : super._( - activeAssetPath: Assets.images.dashBumper.main.active.keyName, - inactiveAssetPath: Assets.images.dashBumper.main.inactive.keyName, + activeAssetPath: Assets.images.dash.bumper.main.active.keyName, + inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName, spriteComponent: SpriteComponent( anchor: Anchor.center, position: Vector2(0, -0.3), @@ -104,8 +104,8 @@ class SmallDashNestBumper extends DashNestBumper { /// {@macro dash_nest_bumper} SmallDashNestBumper.a() : this._( - activeAssetPath: Assets.images.dashBumper.a.active.keyName, - inactiveAssetPath: Assets.images.dashBumper.a.inactive.keyName, + activeAssetPath: Assets.images.dash.bumper.a.active.keyName, + inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, spriteComponent: SpriteComponent( anchor: Anchor.center, position: Vector2(0.35, -1.2), @@ -115,8 +115,8 @@ class SmallDashNestBumper extends DashNestBumper { /// {@macro dash_nest_bumper} SmallDashNestBumper.b() : this._( - activeAssetPath: Assets.images.dashBumper.b.active.keyName, - inactiveAssetPath: Assets.images.dashBumper.b.inactive.keyName, + activeAssetPath: Assets.images.dash.bumper.b.active.keyName, + inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, spriteComponent: SpriteComponent( anchor: Anchor.center, position: Vector2(0.35, -1.2), diff --git a/packages/pinball_components/lib/src/components/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter.dart new file mode 100644 index 00000000..9e9e2dec --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter.dart @@ -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 activate() => _sprite.activate(); + + /// Deactivates this [GoogleLetter]. + Future deactivate() => _sprite.deactivate(); + + @override + Future 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 activate() async { + await add( + _GoogleLetterColorEffect(color: Colors.green), + ); + } + + Future deactivate() async { + await add( + _GoogleLetterColorEffect(color: Colors.red), + ); + } + + @override + Future 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), + ); +} diff --git a/packages/pinball_components/lib/src/components/layer.dart b/packages/pinball_components/lib/src/components/layer.dart index 10477eff..6f027d4c 100644 --- a/packages/pinball_components/lib/src/components/layer.dart +++ b/packages/pinball_components/lib/src/components/layer.dart @@ -21,8 +21,7 @@ mixin Layered on BodyComponent { set layer(Layer value) { _layer = value; if (!isLoaded) { - // TODO(alestiago): Use loaded.whenComplete once provided. - mounted.whenComplete(_applyMaskBits); + loaded.whenComplete(_applyMaskBits); } else { _applyMaskBits(); } diff --git a/lib/game/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart similarity index 73% rename from lib/game/components/plunger.dart rename to packages/pinball_components/lib/src/components/plunger.dart index b8c079b5..7e0ba5ba 100644 --- a/lib/game/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -1,16 +1,14 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/services.dart'; -import 'package:pinball/gen/assets.gen.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/pinball_components.dart'; /// {@template plunger} /// [Plunger] serves as a spring, that shoots the ball on the right side of the /// playfield. /// -/// [Plunger] ignores gravity so the player controls its downward [_pull]. +/// [Plunger] ignores gravity so the player controls its downward [pull]. /// {@endtemplate} -class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { +class Plunger extends BodyComponent with InitialPosition { /// {@macro plunger} Plunger({ required this.compressionDistance, @@ -43,7 +41,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { } /// Set a constant downward velocity on the [Plunger]. - void _pull() { + void pull() { 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 /// from its original [initialPosition]. - void _release() { + void release() { final velocity = (initialPosition.y - body.position.y) * 5; body.linearVelocity = Vector2(0, velocity); } - @override - bool onKeyEvent( - RawKeyEvent event, - Set 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 /// motion. Future _anchorToJoint() async { @@ -97,26 +74,24 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { Future onLoad() async { await super.onLoad(); await _anchorToJoint(); - renderBody = false; - - await _loadSprite(); + await add(_PlungerSpriteComponent()); } +} - Future _loadSprite() async { +class _PlungerSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( - Assets.images.components.plunger.path, + Assets.images.plunger.plunger.keyName, ); - await add( - SpriteComponent( - sprite: sprite, - size: Vector2(5.5, 40), - anchor: Anchor.center, - position: Vector2(2, 19), - angle: -0.033, - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(2, 19); + angle = -0.033; } } @@ -133,14 +108,6 @@ class PlungerAnchor extends JointAnchor { -plunger.compressionDistance, ); } - - @override - Body createBody() { - final bodyDef = BodyDef() - ..position = initialPosition - ..type = BodyType.static; - return world.createBody(bodyDef); - } } /// {@template plunger_anchor_prismatic_joint_def} diff --git a/packages/pinball_components/lib/src/components/score_text.dart b/packages/pinball_components/lib/src/components/score_text.dart new file mode 100644 index 00000000..01b26385 --- /dev/null +++ b/packages/pinball_components/lib/src/components/score_text.dart @@ -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 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(); + } + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 64446faf..1c54095a 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flame_forge2d: ^0.10.0 flutter: sdk: flutter - geometry: + geometry: path: ../geometry @@ -24,6 +24,16 @@ dev_dependencies: flutter: 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/images/ - assets/images/baseboard/ @@ -31,20 +41,25 @@ flutter: - assets/images/dino/ - assets/images/flipper/ - assets/images/launch_ramp/ - - assets/images/dash_bumper/a/ - - assets/images/dash_bumper/b/ - - assets/images/dash_bumper/main/ + - assets/images/dash/ + - assets/images/dash/bumper/a/ + - assets/images/dash/bumper/b/ + - assets/images/dash/bumper/main/ - assets/images/spaceship/ - assets/images/spaceship/rail/ - assets/images/spaceship/ramp/ - assets/images/chrome_dino/ - assets/images/kicker/ + - assets/images/plunger/ - assets/images/slingshot/ + - assets/images/alien_bumper/a/ + - assets/images/alien_bumper/b/ - assets/images/sparky/computer/ - assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/b/ - assets/images/sparky/bumper/c/ - assets/images/backboard/ + - assets/images/google_word/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 62b09402..5cf36b3d 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -23,10 +23,15 @@ void main() { addChromeDinoStories(dashbook); addDashNestBumperStories(dashbook); addKickerStories(dashbook); + addPlungerStories(dashbook); addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); + addAlienZoneStories(dashbook); addZoomStories(dashbook); addBoundariesStories(dashbook); + addGoogleWordStories(dashbook); addLaunchRampStories(dashbook); + addScoreTextStories(dashbook); + runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart new file mode 100644 index 00000000..007d67e1 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart @@ -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 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(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart new file mode 100644 index 00000000..fada1dd9 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart @@ -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 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(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart new file mode 100644 index 00000000..4bc758f9 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart @@ -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, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart index 3b8fe149..2fa4462e 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart @@ -7,7 +7,7 @@ class BallBoosterGame extends LineGame { static const info = ''' Shows how a Ball with a boost works. - Drag to launch a boosted Ball. + - Drag to launch a boosted Ball. '''; @override diff --git a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart index bfc7a9b0..17bd0723 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart @@ -5,7 +5,7 @@ import 'package:sandbox/common/common.dart'; class BasicBallGame extends BasicGame with TapDetector, Traceable { BasicBallGame({ - required this.color, + this.color = Colors.blue, this.ballPriority = 0, this.ballLayer = Layer.all, }); @@ -13,7 +13,7 @@ class BasicBallGame extends BasicGame with TapDetector, Traceable { static const info = ''' 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; diff --git a/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart b/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart index a98fb7b9..0bc2755d 100644 --- a/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart @@ -4,8 +4,6 @@ import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class BoundariesGame extends BasicBallGame with Traceable { - BoundariesGame() : super(color: const Color(0xFFFF0000)); - static const info = ''' Shows how Boundaries are rendered. diff --git a/packages/pinball_components/sandbox/lib/stories/effects/fire_effect_game.dart b/packages/pinball_components/sandbox/lib/stories/effects/fire_effect_game.dart index ecc22910..9e2acaf4 100644 --- a/packages/pinball_components/sandbox/lib/stories/effects/fire_effect_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/effects/fire_effect_game.dart @@ -6,7 +6,7 @@ class FireEffectGame extends LineGame { static const info = ''' Shows how the FireEffect renders. - Drag a line to define the trail direction. + - Drag a line to define the trail direction. '''; @override diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart b/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart index 5a9e1787..c0dfee24 100644 --- a/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart @@ -7,8 +7,6 @@ import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class FlipperGame extends BasicBallGame with KeyboardEvents, Traceable { - FlipperGame() : super(color: Colors.blue); - static const info = ''' Shows how Flippers are rendered. diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart index c1407819..e74abe35 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui'; import 'package:flame_forge2d/flame_forge2d.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'; class BigDashNestBumperGame extends BasicBallGame with Traceable { - BigDashNestBumperGame() : super(color: const Color(0xFF0000FF)); - static const info = ''' Shows how a BigDashNestBumper is rendered. diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart index f3ba7bda..3efb83fe 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui'; import 'package:flame_forge2d/flame_forge2d.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'; class FlutterSignPostGame extends BasicBallGame with Traceable { - FlutterSignPostGame() : super(color: const Color(0xFF0000FF)); - static const info = ''' Shows how a FlutterSignPost is rendered. diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart index a8499581..4ef99b21 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui'; import 'package:flame_forge2d/flame_forge2d.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'; class SmallDashNestBumperAGame extends BasicBallGame with Traceable { - SmallDashNestBumperAGame() : super(color: const Color(0xFF0000FF)); - static const info = ''' Shows how a SmallDashNestBumper ("a") is rendered. diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart index 91b2a383..625b8e5c 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui'; import 'package:flame_forge2d/flame_forge2d.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'; class SmallDashNestBumperBGame extends BasicBallGame with Traceable { - SmallDashNestBumperBGame() : super(color: const Color(0xFF0000FF)); - static const info = ''' Shows how a SmallDashNestBumper ("b") is rendered. diff --git a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart new file mode 100644 index 00000000..ad6e556b --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart @@ -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 onLoad() async { + await super.onLoad(); + addContactCallback(_BallGoogleLetterContactCallback()); + + camera.followVector2(Vector2.zero()); + await add(GoogleLetter(0)); + + await traceAllBodies(); + } +} + +class _BallGoogleLetterContactCallback + extends ContactCallback { + @override + void begin(Ball a, GoogleLetter b, Contact contact) { + super.begin(a, b, contact); + b.activate(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart b/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart new file mode 100644 index 00000000..290bf9dd --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart @@ -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, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart b/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart index 1b29c3f9..add7b205 100644 --- a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart @@ -4,8 +4,6 @@ import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class KickerGame extends BasicBallGame with Traceable { - KickerGame() : super(color: const Color(0xFFFF0000)); - static const info = ''' Shows how Kickers are rendered. diff --git a/packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart b/packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart index a6361094..60d72ff3 100644 --- a/packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart @@ -10,7 +10,7 @@ class BasicLayerGame extends BasicGame with TapDetector { static const info = ''' 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; diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart new file mode 100644 index 00000000..baaab21b --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -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 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 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; + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart b/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart new file mode 100644 index 00000000..86061dc2 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart @@ -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, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/score_text/basic.dart b/packages/pinball_components/sandbox/lib/stories/score_text/basic.dart new file mode 100644 index 00000000..49b83863 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/score_text/basic.dart @@ -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 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)), + ), + ); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart b/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart new file mode 100644 index 00000000..85caef1b --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart @@ -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, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart index 8d54f391..dd1df4de 100644 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart @@ -4,8 +4,6 @@ import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SlingshotGame extends BasicBallGame with Traceable { - SlingshotGame() : super(color: const Color(0xFFFF0000)); - static const info = ''' Shows how Slingshots are rendered. diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart index 97124c3f..95afcd7f 100644 --- a/packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart @@ -9,7 +9,7 @@ class BasicSpaceshipGame extends BasicGame with TapDetector { static const info = ''' 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 diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart index a57beb8d..37537952 100644 --- a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart @@ -6,12 +6,10 @@ import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SparkyBumperGame extends BasicBallGame with Traceable { - SparkyBumperGame() : super(color: const Color(0xFF0000FF)); - static const info = ''' Shows how a SparkyBumper is rendered. - Activate the "trace" parameter to overlay the body. + - Activate the "trace" parameter to overlay the body. '''; @override diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index feea2532..cdcf0825 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,3 +1,4 @@ +export 'alien_zone/stories.dart'; export 'ball/stories.dart'; export 'baseboard/stories.dart'; export 'boundaries/stories.dart'; @@ -5,8 +6,11 @@ export 'chrome_dino/stories.dart'; export 'effects/stories.dart'; export 'flipper/stories.dart'; export 'flutter_forest/stories.dart'; +export 'google_word/stories.dart'; export 'launch_ramp/stories.dart'; export 'layer/stories.dart'; +export 'plunger/stories.dart'; +export 'score_text/stories.dart'; export 'slingshot/stories.dart'; export 'spaceship/stories.dart'; export 'spaceship_rail/stories.dart'; diff --git a/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart b/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart index 276dd39c..7e6d035f 100644 --- a/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart @@ -5,8 +5,9 @@ import 'package:sandbox/common/common.dart'; class BasicCameraZoomGame extends BasicGame with TapDetector { static const info = ''' - Simple game to demonstrate how the CameraZoom can be used. - Tap to zoom in/out + Shows how CameraZoom can be used. + + - Tap to zoom in/out. '''; bool zoomApplied = false; diff --git a/packages/pinball_components/test/src/components/alien_bumper_test.dart b/packages/pinball_components/test/src/components/alien_bumper_test.dart new file mode 100644 index 00000000..cd55b62e --- /dev/null +++ b/packages/pinball_components/test/src/components/alien_bumper_test.dart @@ -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()!; + + 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)), + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/dash_animatronic_test.dart b/packages/pinball_components/test/src/components/dash_animatronic_test.dart new file mode 100644 index 00000000..b268fee0 --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_animatronic_test.dart @@ -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); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/google_letter_test.dart b/packages/pinball_components/test/src/components/google_letter_test.dart new file mode 100644 index 00000000..cdfd3c4a --- /dev/null +++ b/packages/pinball_components/test/src/components/google_letter_test.dart @@ -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())); + expect(() => GoogleLetter(6), throwsA(isA())); + }); + + 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().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().length, + equals(1), + ); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/layer_test.dart b/packages/pinball_components/test/src/components/layer_test.dart index f2372c45..d47702ea 100644 --- a/packages/pinball_components/test/src/components/layer_test.dart +++ b/packages/pinball_components/test/src/components/layer_test.dart @@ -57,8 +57,6 @@ void main() { const expectedLayer = Layer.spaceshipEntranceRamp; final component = TestLayeredBodyComponent()..layer = expectedLayer; await game.ensureAdd(component); - // TODO(alestiago): modify once component.loaded is available. - await component.mounted; _expectLayerOnFixtures( fixtures: component.body.fixtures, @@ -79,8 +77,6 @@ void main() { component.layer = expectedLayer; await game.ensureAdd(component); - // TODO(alestiago): modify once component.loaded is available. - await component.mounted; _expectLayerOnFixtures( fixtures: component.body.fixtures, diff --git a/test/game/components/plunger_test.dart b/packages/pinball_components/test/src/components/plunger_test.dart similarity index 62% rename from test/game/components/plunger_test.dart rename to packages/pinball_components/test/src/components/plunger_test.dart index 65789ae0..8f8a26db 100644 --- a/test/game/components/plunger_test.dart +++ b/packages/pinball_components/test/src/components/plunger_test.dart @@ -1,12 +1,9 @@ // ignore_for_file: cascade_invocations -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'; @@ -117,13 +114,23 @@ void main() { ); }); - group('onKeyEvent', () { - final keys = UnmodifiableListView([ - LogicalKeyboardKey.space, - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.keyS, - ]); + group('pull', () { + flameTester.test( + 'moves downwards when pull is called', + (game) async { + 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; setUp(() { @@ -132,56 +139,28 @@ void main() { ); }); - testRawKeyUpEvents(keys, (event) { - final keyLabel = (event.logicalKey != LogicalKeyboardKey.space) - ? event.logicalKey.keyLabel - : 'Space'; - flameTester.test( - 'moves upwards when $keyLabel is released ' - 'and plunger is below its starting position', - (game) async { - await game.ensureAdd(plunger); - plunger.body.setTransform(Vector2(0, -1), 0); - plunger.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isPositive); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); + flameTester.test( + 'moves upwards when release is called ' + 'and plunger is below its starting position', (game) async { + await game.ensureAdd(plunger); + plunger.body.setTransform(Vector2(0, -1), 0); + plunger.release(); - testRawKeyUpEvents(keys, (event) { - final keyLabel = (event.logicalKey != LogicalKeyboardKey.space) - ? event.logicalKey.keyLabel - : 'Space'; - flameTester.test( - 'does not move when $keyLabel is released ' - 'and plunger is in its starting position', - (game) async { - await game.ensureAdd(plunger); - plunger.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isZero); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); + expect(plunger.body.linearVelocity.y, isPositive); + 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); - }, - ); - }); + flameTester.test( + 'does not move when release is called ' + 'and plunger is in its starting position', + (game) async { + await game.ensureAdd(plunger); + plunger.release(); + + expect(plunger.body.linearVelocity.y, isZero); + expect(plunger.body.linearVelocity.x, isZero); + }, + ); }); }); @@ -210,11 +189,13 @@ void main() { group('PlungerAnchorPrismaticJointDef', () { const compressionDistance = 10.0; late Plunger plunger; + late PlungerAnchor anchor; setUp(() { plunger = Plunger( compressionDistance: compressionDistance, ); + anchor = PlungerAnchor(plunger: plunger); }); group('initializes with', () { @@ -222,7 +203,6 @@ void main() { 'plunger body as bodyA', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -238,7 +218,6 @@ void main() { 'anchor body as bodyB', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -255,7 +234,6 @@ void main() { 'limits enabled', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -272,7 +250,6 @@ void main() { 'lower translation limit as negative infinity', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -289,7 +266,6 @@ void main() { 'connected body collison enabled', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -303,53 +279,47 @@ void main() { ); }); - testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { - late final anchor = PlungerAnchor(plunger: plunger); - flameTester.testGameWidget( - 'plunger cannot go below anchor', - setUp: (game, tester) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); + flameTester.testGameWidget( + 'plunger cannot go below anchor', + setUp: (game, tester) async { + await game.ensureAdd(plunger); + await game.ensureAdd(anchor); - // Giving anchor a shape for the plunger to collide with. - anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1)); + // Giving anchor a shape for the plunger to collide with. + anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1)); - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(PrismaticJoint(jointDef)); - await tester.pump(const Duration(seconds: 1)); - }, - verify: (game, tester) async { - expect(plunger.body.position.y > anchor.body.position.y, isTrue); - }, - ); - }); + await tester.pump(const Duration(seconds: 1)); + }, + verify: (game, tester) async { + expect(plunger.body.position.y > anchor.body.position.y, isTrue); + }, + ); - testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { - flameTester.testGameWidget( - 'plunger cannot excessively exceed starting position', - setUp: (game, tester) async { - await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); - await game.ensureAdd(anchor); + flameTester.testGameWidget( + 'plunger cannot excessively exceed starting position', + setUp: (game, tester) async { + await game.ensureAdd(plunger); + await game.ensureAdd(anchor); - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(PrismaticJoint(jointDef)); - plunger.body.setTransform(Vector2(0, -1), 0); + plunger.body.setTransform(Vector2(0, -1), 0); - await tester.pump(const Duration(seconds: 1)); - }, - verify: (game, tester) async { - expect(plunger.body.position.y < 1, isTrue); - }, - ); - }); + await tester.pump(const Duration(seconds: 1)); + }, + verify: (game, tester) async { + expect(plunger.body.position.y < 1, isTrue); + }, + ); }); } diff --git a/packages/pinball_components/test/src/components/score_text_effects_test.dart b/packages/pinball_components/test/src/components/score_text_effects_test.dart new file mode 100644 index 00000000..7f828f1d --- /dev/null +++ b/packages/pinball_components/test/src/components/score_text_effects_test.dart @@ -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().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().first; + expect(text.firstChild(), 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)); + }, + ); + }); +} diff --git a/test/game/components/alien_zone_test.dart b/test/game/components/alien_zone_test.dart new file mode 100644 index 00000000..68a2c2f1 --- /dev/null +++ b/test/game/components/alien_zone_test.dart @@ -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().length, + equals(2), + ); + }, + ); + }); + + group('bumpers', () { + late ControlledAlienBumper controlledAlienBumper; + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + 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.empty(), + initialState: const GameState.initial(), + ); + + await game.ensureAdd(alienZone); + await game.ensureAdd(ball); + game.addContactCallback(BallScorePointsCallback(game)); + + final bumpers = alienZone.descendants().whereType(); + + for (final bumper in bumpers) { + beginContact(game, bumper, ball); + verify( + () => gameBloc.add( + Scored(points: bumper.points), + ), + ).called(1); + } + }, + ); + }); + }); +} diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart new file mode 100644 index 00000000..bb965fc1 --- /dev/null +++ b/test/game/components/controlled_plunger_test.dart @@ -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); + }, + ); + }); + }); + }); +} diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index d85fe54f..e9e58985 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -18,7 +18,6 @@ void main() { flameTester.test( 'loads correctly', (game) async { - await game.ready(); final flutterForest = FlutterForest(); await game.ensureAdd(flutterForest); @@ -30,7 +29,6 @@ void main() { flameTester.test( 'a FlutterSignPost', (game) async { - await game.ready(); final flutterForest = 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(), + isNotNull, + ); + }, + ); + flameTester.test( 'a BigDashNestBumper', (game) async { - await game.ready(); final flutterForest = FlutterForest(); await game.ensureAdd(flutterForest); @@ -58,7 +68,6 @@ void main() { flameTester.test( 'two SmallDashNestBumper', (game) async { - await game.ready(); final flutterForest = FlutterForest(); await game.ensureAdd(flutterForest); @@ -102,14 +111,15 @@ void main() { }); flameTester.test( - 'onNewState adds a new ball', + 'onNewState adds a new ball after a duration', (game) async { final flutterForest = FlutterForest(); - await game.ready(); await game.ensureAdd(flutterForest); final previousBalls = game.descendants().whereType().length; flutterForest.controller.onNewState(MockGameState()); + + await Future.delayed(const Duration(milliseconds: 700)); await game.ready(); 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().single; + + expect(dashAnimatronic.playing, isTrue); + }, + ); + group('bumpers', () { late Ball ball; late GameBloc gameBloc; diff --git a/test/game/components/score_effect_controller_test.dart b/test/game/components/score_effect_controller_test.dart new file mode 100644 index 00000000..241f040b --- /dev/null +++ b/test/game/components/score_effect_controller_test.dart @@ -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')); + }); + }); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 2dfd5d76..ef55b399 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -70,6 +70,14 @@ void main() { }, ); + flameTester.test( + 'one AlienZone', + (game) async { + await game.ready(); + expect(game.children.whereType().length, equals(1)); + }, + ); + group('controller', () { // TODO(alestiago): Write test to be controller agnostic. group('listenWhen', () {