diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 31d3b917..f91d9baf 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -7,6 +7,7 @@ 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/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/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart index 9d88f0f5..ee8da614 100644 --- a/lib/game/components/sparky_fire_zone.dart +++ b/lib/game/components/sparky_fire_zone.dart @@ -1,21 +1,67 @@ // 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'; -// TODO(ruimiguel): create and add SparkyFireZone component here in other PR. +/// {@template sparky_fire_zone} +/// Area positioned at the top left of the [Board] where the [Ball] +/// can bounce off [SparkyBumper]s. +/// +/// When a [Ball] hits [SparkyBumper]s, they toggle between activated and +/// deactivated states. +/// {@endtemplate} +class SparkyFireZone extends Component with HasGameRef { + /// {@macro sparky_fire_zone} + SparkyFireZone(); + + @override + Future onLoad() async { + await super.onLoad(); + + gameRef.addContactCallback(_ControlledSparkyBumperBallContactCallback()); -// TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done -// ignore: public_member_api_docs + final lowerLeftBumper = ControlledSparkyBumper.a() + ..initialPosition = Vector2(-23.15, 41.65); + final upperLeftBumper = ControlledSparkyBumper.b() + ..initialPosition = Vector2(-21.25, 58.15); + final rightBumper = ControlledSparkyBumper.c() + ..initialPosition = Vector2(-3.56, 53.051); + + await addAll([ + lowerLeftBumper, + upperLeftBumper, + rightBumper, + ]); + } +} + +/// {@template controlled_sparky_bumper} +/// [SparkyBumper] with [_SparkyBumperController] attached. +/// {@endtemplate} +@visibleForTesting class ControlledSparkyBumper extends SparkyBumper - with Controls<_SparkyBumperController> { - // TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done - // ignore: public_member_api_docs - ControlledSparkyBumper() : super.a() { + with Controls<_SparkyBumperController>, ScorePoints { + ///{@macro controlled_sparky_bumper} + ControlledSparkyBumper.a() : super.a() { + controller = _SparkyBumperController(this); + } + + ///{@macro controlled_sparky_bumper} + ControlledSparkyBumper.b() : super.b() { + controller = _SparkyBumperController(this); + } + + ///{@macro controlled_sparky_bumper} + ControlledSparkyBumper.c() : super.c() { controller = _SparkyBumperController(this); } + + @override + int get points => 20; } /// {@template sparky_bumper_controller} @@ -42,3 +88,16 @@ class _SparkyBumperController extends ComponentController isActivated = !isActivated; } } + +/// Listens when a [Ball] bounces bounces against a [SparkyBumper]. +class _ControlledSparkyBumperBallContactCallback + extends ContactCallback, Ball> { + @override + void begin( + Controls<_SparkyBumperController> controlledSparkyBumper, + Ball _, + Contact __, + ) { + controlledSparkyBumper.controller.hit(); + } +} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 49992653..8d080b22 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)))); @@ -52,6 +53,7 @@ class PinballGame extends Forge2DGame await add(plunger); unawaited(add(Board())); + unawaited(add(SparkyFireZone())); unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(DinoWalls())); unawaited(_addBonusWord()); diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 90013646..b3b964f3 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -3,8 +3,6 @@ /// FlutterGen /// ***************************************************** -// ignore_for_file: directives_ordering,unnecessary_import - import 'package:flutter/widgets.dart'; class $AssetsImagesGen { @@ -17,11 +15,8 @@ class $AssetsImagesGen { class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); - /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); - - /// File path: assets/images/components/plunger.png AssetGenImage get plunger => const AssetGenImage('assets/images/components/plunger.png'); } 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/fonts.gen.dart b/packages/pinball_components/lib/gen/fonts.gen.dart new file mode 100644 index 00000000..b15f2dd0 --- /dev/null +++ b/packages/pinball_components/lib/gen/fonts.gen.dart @@ -0,0 +1,11 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +class FontFamily { + FontFamily._(); + + static const String pixeloidMono = 'PixeloidMono'; + 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/components.dart b/packages/pinball_components/lib/src/components/components.dart index b4ba70e2..4b2b41e7 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -17,6 +17,7 @@ export 'kicker.dart'; export 'launch_ramp.dart'; export 'layer.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/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index 2eea7a91..deaa3941 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -11,6 +11,9 @@ import 'package:pinball_components/pinball_components.dart'; /// [_LaunchRampForegroundRailing]. /// {@endtemplate} class LaunchRamp extends Forge2DBlueprint { + /// Base priority for [Ball] while inside [LaunchRamp]. + static const ballPriorityInsideRamp = 0; + @override void build(_) { addAllContactCallback([ @@ -40,7 +43,10 @@ class LaunchRamp extends Forge2DBlueprint { /// {@endtemplate} class _LaunchRampBase extends BodyComponent with InitialPosition, Layered { /// {@macro launch_ramp_base} - _LaunchRampBase() : super(priority: -1) { + _LaunchRampBase() + : super( + priority: LaunchRamp.ballPriorityInsideRamp - 1, + ) { layer = Layer.launcher; } @@ -143,7 +149,10 @@ class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { class _LaunchRampForegroundRailing extends BodyComponent with InitialPosition, Layered { /// {@macro launch_ramp_foreground_railing} - _LaunchRampForegroundRailing() : super(priority: 1) { + _LaunchRampForegroundRailing() + : super( + priority: LaunchRamp.ballPriorityInsideRamp + 1, + ) { layer = Layer.launcher; } @@ -227,7 +236,7 @@ class _LaunchRampExit extends RampOpening { super( insideLayer: Layer.launcher, orientation: RampOrientation.down, - insidePriority: 3, + insidePriority: LaunchRamp.ballPriorityInsideRamp, ); final double _rotation; 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/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index 10144eef..6643a53a 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -37,7 +37,7 @@ class Spaceship extends Forge2DBlueprint { AndroidHead()..initialPosition = position, SpaceshipHole( outsideLayer: Layer.spaceshipExitRail, - outsidePriority: SpaceshipRail.ballPriorityWhenOnSpaceshipRail, + outsidePriority: SpaceshipRail.ballPriorityInsideRail, )..initialPosition = position - Vector2(5.2, 4.8), SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8), SpaceshipWall()..initialPosition = position, diff --git a/packages/pinball_components/lib/src/components/spaceship_rail.dart b/packages/pinball_components/lib/src/components/spaceship_rail.dart index 2cc8bccc..b63e401a 100644 --- a/packages/pinball_components/lib/src/components/spaceship_rail.dart +++ b/packages/pinball_components/lib/src/components/spaceship_rail.dart @@ -14,8 +14,8 @@ class SpaceshipRail extends Forge2DBlueprint { /// {@macro spaceship_rail} SpaceshipRail(); - /// Base priority for ball while be in [_SpaceshipRailRamp]. - static const ballPriorityWhenOnSpaceshipRail = 2; + /// Base priority for [Ball] while inside [SpaceshipRail]. + static const ballPriorityInsideRail = 2; @override void build(_) { @@ -45,9 +45,8 @@ class SpaceshipRail extends Forge2DBlueprint { class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { _SpaceshipRailRamp() : super( - priority: SpaceshipRail.ballPriorityWhenOnSpaceshipRail - 1, + priority: SpaceshipRail.ballPriorityInsideRail - 1, ) { - renderBody = false; layer = Layer.spaceshipExitRail; } @@ -139,6 +138,8 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { @override Future onLoad() async { await super.onLoad(); + renderBody = false; + await add(_SpaceshipRailRampSpriteComponent()); } } @@ -161,11 +162,7 @@ class _SpaceshipRailRampSpriteComponent extends SpriteComponent class _SpaceshipRailForeground extends SpriteComponent with HasGameRef { _SpaceshipRailForeground() - : super( - anchor: Anchor.center, - position: Vector2(-28.5, 19.7), - priority: SpaceshipRail.ballPriorityWhenOnSpaceshipRail + 1, - ); + : super(priority: SpaceshipRail.ballPriorityInsideRail + 1); @override Future onLoad() async { @@ -176,6 +173,8 @@ class _SpaceshipRailForeground extends SpriteComponent with HasGameRef { ); this.sprite = sprite; size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(-28.5, 19.7); } } @@ -183,7 +182,7 @@ class _SpaceshipRailForeground extends SpriteComponent with HasGameRef { class _SpaceshipRailBase extends BodyComponent with InitialPosition, Layered { _SpaceshipRailBase({required this.radius}) : super( - priority: SpaceshipRail.ballPriorityWhenOnSpaceshipRail + 1, + priority: SpaceshipRail.ballPriorityInsideRail + 1, ) { renderBody = false; layer = Layer.board; @@ -213,9 +212,9 @@ class SpaceshipRailExit extends RampOpening { /// {@macro spaceship_rail_exit} SpaceshipRailExit() : super( - insideLayer: Layer.spaceshipExitRail, orientation: RampOrientation.down, - insidePriority: 3, + insideLayer: Layer.spaceshipExitRail, + insidePriority: SpaceshipRail.ballPriorityInsideRail, ) { renderBody = false; layer = Layer.spaceshipExitRail; @@ -224,10 +223,10 @@ class SpaceshipRailExit extends RampOpening { @override Shape get shape { return ArcShape( - center: Vector2(-28, -19), + center: Vector2(-29, -19), arcRadius: 2.5, angle: math.pi * 0.4, - rotation: -0.16, + rotation: 0.26, ); } } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp.dart index 773b0441..452d101e 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp.dart @@ -184,8 +184,6 @@ class _SpaceshipRampForegroundRailing extends BodyComponent @override Body createBody() { - renderBody = false; - final bodyDef = BodyDef() ..userData = this ..position = initialPosition; @@ -199,11 +197,13 @@ class _SpaceshipRampForegroundRailing extends BodyComponent @override Future onLoad() async { await super.onLoad(); - await add(_SpaceshipRampForegroundRalingSpriteComponent()); + renderBody = false; + + await add(_SpaceshipRampForegroundRailingSpriteComponent()); } } -class _SpaceshipRampForegroundRalingSpriteComponent extends SpriteComponent +class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { @@ -221,13 +221,12 @@ class _SpaceshipRampForegroundRalingSpriteComponent extends SpriteComponent /// Represents the ground right base of the [SpaceshipRamp]. class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered { _SpaceshipRampBase() { + renderBody = false; layer = Layer.board; } @override Body createBody() { - renderBody = false; - const baseWidth = 6; final baseShape = BezierCurveShape( controlPoints: [ @@ -266,7 +265,9 @@ class _SpaceshipRampOpening extends RampOpening { orientation: RampOrientation.down, insidePriority: SpaceshipRamp.ballPriorityInsideRamp, outsidePriority: outsidePriority, - ); + ) { + renderBody = false; + } final double _rotation; @@ -274,7 +275,6 @@ class _SpaceshipRampOpening extends RampOpening { @override Shape get shape { - renderBody = false; return PolygonShape() ..setAsBox( _size.x, diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 64446faf..7a5d3e49 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/ diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index b3b331a7..1c9c9c25 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -25,5 +25,10 @@ void main() { addSparkyBumperStories(dashbook); addZoomStories(dashbook); addBoundariesStories(dashbook); + addSpaceshipRampStories(dashbook); + addSpaceshipRailStories(dashbook); + addLaunchRampStories(dashbook); + addScoreTextStories(dashbook); + runApp(dashbook); } 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 ee9fa88c..bfc7a9b0 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 @@ -4,7 +4,11 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; class BasicBallGame extends BasicGame with TapDetector, Traceable { - BasicBallGame({required this.color}); + BasicBallGame({ + required this.color, + this.ballPriority = 0, + this.ballLayer = Layer.all, + }); static const info = ''' Shows how a Ball works. @@ -13,11 +17,16 @@ class BasicBallGame extends BasicGame with TapDetector, Traceable { '''; final Color color; + final int ballPriority; + final Layer ballLayer; @override void onTapUp(TapUpInfo info) { add( - Ball(baseColor: color)..initialPosition = info.eventPosition.game, + Ball(baseColor: color) + ..initialPosition = info.eventPosition.game + ..layer = ballLayer + ..priority = ballPriority, ); traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart new file mode 100644 index 00000000..5258de86 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class LaunchRampGame extends BasicBallGame { + LaunchRampGame() + : super( + color: Colors.blue, + ballPriority: LaunchRamp.ballPriorityInsideRamp, + ballLayer: Layer.launcher, + ); + + static const info = ''' + Shows how LaunchRamp are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera + ..followVector2(Vector2(0, 0)) + ..zoom = 7.5; + + final launchRamp = LaunchRamp(); + unawaited(addFromBlueprint(launchRamp)); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/launch_ramp/stories.dart b/packages/pinball_components/sandbox/lib/stories/launch_ramp/stories.dart new file mode 100644 index 00000000..083a4584 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/launch_ramp/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/launch_ramp/launch_ramp_game.dart'; + +void addLaunchRampStories(Dashbook dashbook) { + dashbook.storiesOf('LaunchRamp').add( + 'Basic', + (context) => GameWidget( + game: LaunchRampGame()..trace = context.boolProperty('Trace', true), + ), + codeLink: buildSourceLink('launch_ramp/basic.dart'), + info: LaunchRampGame.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/spaceship_rail/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/spaceship_rail/spaceship_rail_game.dart new file mode 100644 index 00000000..cef04304 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/spaceship_rail/spaceship_rail_game.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SpaceshipRailGame extends BasicBallGame { + SpaceshipRailGame() + : super( + color: Colors.blue, + ballPriority: SpaceshipRail.ballPriorityInsideRail, + ballLayer: Layer.spaceshipExitRail, + ); + + static const info = ''' + Shows how SpaceshipRail are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2(-30, -10)); + + final spaceshipRail = SpaceshipRail(); + unawaited(addFromBlueprint(spaceshipRail)); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_rail/stories.dart b/packages/pinball_components/sandbox/lib/stories/spaceship_rail/stories.dart new file mode 100644 index 00000000..e69ed1db --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/spaceship_rail/stories.dart @@ -0,0 +1,16 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/spaceship_rail/spaceship_rail_game.dart'; + +void addSpaceshipRailStories(Dashbook dashbook) { + dashbook.storiesOf('SpaceshipRail').add( + 'Basic', + (context) => GameWidget( + game: SpaceshipRailGame() + ..trace = context.boolProperty('Trace', true), + ), + codeLink: buildSourceLink('spaceship_rail/basic.dart'), + info: SpaceshipRailGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/spaceship_ramp_game.dart new file mode 100644 index 00000000..e3850da3 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/spaceship_ramp_game.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SpaceshipRampGame extends BasicBallGame { + SpaceshipRampGame() + : super( + color: Colors.blue, + ballPriority: SpaceshipRamp.ballPriorityInsideRamp, + ballLayer: Layer.spaceshipEntranceRamp, + ); + + static const info = ''' + Shows how SpaceshipRamp are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2(-10, -20)); + + final spaceshipRamp = SpaceshipRamp(); + unawaited(addFromBlueprint(spaceshipRamp)); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/stories.dart b/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/stories.dart new file mode 100644 index 00000000..f0aeadff --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/stories.dart @@ -0,0 +1,16 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/spaceship_ramp/spaceship_ramp_game.dart'; + +void addSpaceshipRampStories(Dashbook dashbook) { + dashbook.storiesOf('SpaceshipRamp').add( + 'Basic', + (context) => GameWidget( + game: SpaceshipRampGame() + ..trace = context.boolProperty('Trace', true), + ), + codeLink: buildSourceLink('spaceship_ramp/basic.dart'), + info: SpaceshipRampGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 009f53ac..b4400500 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -5,8 +5,12 @@ export 'chrome_dino/stories.dart'; export 'effects/stories.dart'; export 'flipper/stories.dart'; export 'flutter_forest/stories.dart'; +export 'launch_ramp/stories.dart'; export 'layer/stories.dart'; +export 'score_text/stories.dart'; export 'slingshot/stories.dart'; export 'spaceship/stories.dart'; +export 'spaceship_rail/stories.dart'; +export 'spaceship_ramp/stories.dart'; export 'sparky_bumper/stories.dart'; export 'zoom/stories.dart'; 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/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/components/sparky_fire_zone_test.dart b/test/game/components/sparky_fire_zone_test.dart index dceaa9cc..da8d8404 100644 --- a/test/game/components/sparky_fire_zone_test.dart +++ b/test/game/components/sparky_fire_zone_test.dart @@ -1,8 +1,13 @@ // 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'; @@ -11,13 +16,57 @@ void main() { final flameTester = FlameTester(EmptyPinballGameTest.new); group('SparkyFireZone', () { + flameTester.test( + 'loads correctly', + (game) async { + await game.ready(); + final sparkyFireZone = SparkyFireZone(); + await game.ensureAdd(sparkyFireZone); + + expect(game.contains(sparkyFireZone), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'three SparkyBumper', + (game) async { + await game.ready(); + final sparkyFireZone = SparkyFireZone(); + await game.ensureAdd(sparkyFireZone); + + expect( + sparkyFireZone.descendants().whereType().length, + equals(3), + ); + }, + ); + }); + group('bumpers', () { late ControlledSparkyBumper controlledSparkyBumper; + late Ball ball; + late GameBloc gameBloc; + + setUp(() { + ball = Ball(baseColor: const Color(0xFF00FFFF)); + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballGameTest.new, + blocBuilder: () => gameBloc, + ); flameTester.testGameWidget( 'activate when deactivated bumper is hit', setUp: (game, tester) async { - controlledSparkyBumper = ControlledSparkyBumper(); + controlledSparkyBumper = ControlledSparkyBumper.a(); await game.ensureAdd(controlledSparkyBumper); controlledSparkyBumper.controller.hit(); @@ -30,7 +79,7 @@ void main() { flameTester.testGameWidget( 'deactivate when activated bumper is hit', setUp: (game, tester) async { - controlledSparkyBumper = ControlledSparkyBumper(); + controlledSparkyBumper = ControlledSparkyBumper.a(); await game.ensureAdd(controlledSparkyBumper); controlledSparkyBumper.controller.hit(); @@ -40,6 +89,27 @@ void main() { expect(controlledSparkyBumper.controller.isActivated, isFalse); }, ); + + flameBlocTester.testGameWidget( + 'add Scored event', + setUp: (game, tester) async { + final sparkyFireZone = SparkyFireZone(); + await game.ensureAdd(sparkyFireZone); + await game.ensureAdd(ball); + game.addContactCallback(BallScorePointsCallback(game)); + + final bumpers = sparkyFireZone.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/pinball_game_test.dart b/test/game/pinball_game_test.dart index d83bb396..2dfd5d76 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -62,6 +62,14 @@ void main() { ); }); + flameTester.test( + 'one SparkyFireZone', + (game) async { + await game.ready(); + expect(game.children.whereType().length, equals(1)); + }, + ); + group('controller', () { // TODO(alestiago): Write test to be controller agnostic. group('listenWhen', () {