diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 4103bb81..9dc81135 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -67,7 +67,9 @@ class BallController extends ComponentController const Duration(milliseconds: 2583), ); component.resume(); - await component.boost(Vector2(40, 110)); + await component.add( + BallTurboChargingBehavior(impulse: Vector2(40, 110)), + ); } @override diff --git a/lib/game/components/launcher.dart b/lib/game/components/launcher.dart index ffac6507..da1a3569 100644 --- a/lib/game/components/launcher.dart +++ b/lib/game/components/launcher.dart @@ -12,6 +12,7 @@ class Launcher extends Component { : super( children: [ LaunchRamp(), + Flapper(), ControlledPlunger(compressionDistance: 9.2) ..initialPosition = Vector2(41.2, 43.7), RocketSpriteComponent()..position = Vector2(43, 62.3), diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 2a847ce0..22c1c2d6 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -130,6 +130,9 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.score.twentyThousand.keyName), images.load(components.Assets.images.score.twoHundredThousand.keyName), images.load(components.Assets.images.score.oneMillion.keyName), + images.load(components.Assets.images.flapper.backSupport.keyName), + images.load(components.Assets.images.flapper.frontSupport.keyName), + images.load(components.Assets.images.flapper.flap.keyName), images.load(dashTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName), diff --git a/packages/pinball_components/assets/images/flapper/back-support.png b/packages/pinball_components/assets/images/flapper/back-support.png new file mode 100644 index 00000000..74b3ae84 Binary files /dev/null and b/packages/pinball_components/assets/images/flapper/back-support.png differ diff --git a/packages/pinball_components/assets/images/flapper/flap.png b/packages/pinball_components/assets/images/flapper/flap.png new file mode 100644 index 00000000..3860df17 Binary files /dev/null and b/packages/pinball_components/assets/images/flapper/flap.png differ diff --git a/packages/pinball_components/assets/images/flapper/front-support.png b/packages/pinball_components/assets/images/flapper/front-support.png new file mode 100644 index 00000000..c3b7ca1e Binary files /dev/null and b/packages/pinball_components/assets/images/flapper/front-support.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index da9747c4..73dd4614 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -22,6 +22,7 @@ class $AssetsImagesGen { $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); + $AssetsImagesFlapperGen get flapper => const $AssetsImagesFlapperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesGoogleWordGen get googleWord => const $AssetsImagesGoogleWordGen(); @@ -133,6 +134,22 @@ class $AssetsImagesDinoGen { const AssetGenImage('assets/images/dino/top-wall.png'); } +class $AssetsImagesFlapperGen { + const $AssetsImagesFlapperGen(); + + /// File path: assets/images/flapper/back-support.png + AssetGenImage get backSupport => + const AssetGenImage('assets/images/flapper/back-support.png'); + + /// File path: assets/images/flapper/flap.png + AssetGenImage get flap => + const AssetGenImage('assets/images/flapper/flap.png'); + + /// File path: assets/images/flapper/front-support.png + AssetGenImage get frontSupport => + const AssetGenImage('assets/images/flapper/front-support.png'); +} + class $AssetsImagesFlipperGen { const $AssetsImagesFlipperGen(); diff --git a/packages/pinball_components/lib/src/components/ball/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart index 12b1c877..456dd4f1 100644 --- a/packages/pinball_components/lib/src/components/ball/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -1,14 +1,13 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/widgets.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_components/src/components/ball/behaviors/ball_gravitating_behavior.dart'; -import 'package:pinball_components/src/components/ball/behaviors/ball_scaling_behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'behaviors/behaviors.dart'; + /// {@template ball} /// A solid, [BodyType.dynamic] sphere that rolls and bounces around. /// {@endtemplate} @@ -81,12 +80,6 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { void resume() { body.gravityScale = Vector2(1, 1); } - - /// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball]. - Future boost(Vector2 impulse) async { - body.linearVelocity = impulse; - await add(_TurboChargeSpriteAnimationComponent()); - } } class _BallSpriteComponent extends SpriteComponent with HasGameRef { @@ -101,55 +94,3 @@ class _BallSpriteComponent extends SpriteComponent with HasGameRef { anchor = Anchor.center; } } - -class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent - with HasGameRef, ZIndex { - _TurboChargeSpriteAnimationComponent() - : super( - anchor: const Anchor(0.53, 0.72), - removeOnFinish: true, - ) { - zIndex = ZIndexes.turboChargeFlame; - } - - late final Vector2 _textureSize; - - @override - Future onLoad() async { - await super.onLoad(); - - final spriteSheet = await gameRef.images.load( - Assets.images.ball.flameEffect.keyName, - ); - - const amountPerRow = 8; - const amountPerColumn = 4; - _textureSize = Vector2( - spriteSheet.width / amountPerRow, - spriteSheet.height / amountPerColumn, - ); - - 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 (parent != null) { - final body = (parent! as BodyComponent).body; - final direction = -body.linearVelocity.normalized(); - angle = math.atan2(direction.x, -direction.y); - size = (_textureSize / 45) * body.fixtures.first.shape.radius; - } - } -} diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/ball_turbo_charging_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_turbo_charging_behavior.dart new file mode 100644 index 00000000..f1e5a855 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_turbo_charging_behavior.dart @@ -0,0 +1,81 @@ +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ball_turbo_charging_behavior} +/// Puts the [Ball] in flames and [_impulse]s it. +/// {@endtemplate} +class BallTurboChargingBehavior extends TimerComponent with ParentIsA { + /// {@macro ball_turbo_charging_behavior} + BallTurboChargingBehavior({ + required Vector2 impulse, + }) : _impulse = impulse, + super(period: 5, removeOnFinish: true); + + final Vector2 _impulse; + + @override + Future onLoad() async { + await super.onLoad(); + + parent.body.linearVelocity = _impulse; + await parent.add(_TurboChargeSpriteAnimationComponent()); + } + + @override + void onRemove() { + parent + .firstChild<_TurboChargeSpriteAnimationComponent>()! + .removeFromParent(); + super.onRemove(); + } +} + +class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef, ZIndex, ParentIsA { + _TurboChargeSpriteAnimationComponent() + : super( + anchor: const Anchor(0.53, 0.72), + ) { + zIndex = ZIndexes.turboChargeFlame; + } + + late final Vector2 _textureSize; + + @override + void update(double dt) { + super.update(dt); + + final direction = -parent.body.linearVelocity.normalized(); + angle = math.atan2(direction.x, -direction.y); + size = (_textureSize / 45) * parent.body.fixtures.first.shape.radius; + } + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = await gameRef.images.load( + Assets.images.ball.flameEffect.keyName, + ); + + const amountPerRow = 8; + const amountPerColumn = 4; + _textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: _textureSize, + ), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart index 038b7833..1068a20e 100644 --- a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart +++ b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart @@ -1,2 +1,3 @@ export 'ball_gravitating_behavior.dart'; export 'ball_scaling_behavior.dart'; +export 'ball_turbo_charging_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 60faef43..6417b794 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -14,6 +14,7 @@ export 'dash_animatronic.dart'; export 'dash_nest_bumper/dash_nest_bumper.dart'; export 'dino_walls.dart'; export 'fire_effect.dart'; +export 'flapper/flapper.dart'; export 'flipper.dart'; export 'google_letter/google_letter.dart'; export 'initial_position.dart'; diff --git a/packages/pinball_components/lib/src/components/flapper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/flapper/behaviors/behaviors.dart new file mode 100644 index 00000000..573578e5 --- /dev/null +++ b/packages/pinball_components/lib/src/components/flapper/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'flapper_spinning_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/flapper/behaviors/flapper_spinning_behavior.dart b/packages/pinball_components/lib/src/components/flapper/behaviors/flapper_spinning_behavior.dart new file mode 100644 index 00000000..9a4e2a99 --- /dev/null +++ b/packages/pinball_components/lib/src/components/flapper/behaviors/flapper_spinning_behavior.dart @@ -0,0 +1,15 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class FlapperSpinningBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.parent?.firstChild()?.playing = true; + } +} diff --git a/packages/pinball_components/lib/src/components/flapper/flapper.dart b/packages/pinball_components/lib/src/components/flapper/flapper.dart new file mode 100644 index 00000000..f336273e --- /dev/null +++ b/packages/pinball_components/lib/src/components/flapper/flapper.dart @@ -0,0 +1,215 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/flapper/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template flapper} +/// Flap to let a [Ball] out of the [LaunchRamp] and to prevent [Ball]s from +/// going back in. +/// {@endtemplate} +class Flapper extends Component { + /// {@macro flapper} + Flapper() + : super( + children: [ + FlapperEntrance( + children: [ + FlapperSpinningBehavior(), + ], + )..initialPosition = Vector2(4, -69.3), + _FlapperStructure(), + _FlapperExit()..initialPosition = Vector2(-0.6, -33.8), + _BackSupportSpriteComponent(), + _FrontSupportSpriteComponent(), + FlapSpriteAnimationComponent(), + ], + ); + + /// Creates a [Flapper] without any children. + /// + /// This can be used for testing [Flapper]'s behaviors in isolation. + @visibleForTesting + Flapper.test(); +} + +/// {@template flapper_entrance} +/// Sensor used in [FlapperSpinningBehavior] to animate +/// [FlapSpriteAnimationComponent]. +/// {@endtemplate} +class FlapperEntrance extends BodyComponent with InitialPosition, Layered { + /// {@macro flapper_entrance} + FlapperEntrance({ + Iterable? children, + }) : super( + children: children, + renderBody: false, + ) { + layer = Layer.launcher; + } + + @override + Body createBody() { + final shape = EdgeShape() + ..set( + Vector2.zero(), + Vector2(0, 3.2), + ); + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef(position: initialPosition); + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _FlapperStructure extends BodyComponent with Layered { + _FlapperStructure() : super(renderBody: false) { + layer = Layer.board; + } + + List _createFixtureDefs() { + final leftEdgeShape = EdgeShape() + ..set( + Vector2(1.9, -69.3), + Vector2(1.9, -66), + ); + + final bottomEdgeShape = EdgeShape() + ..set( + leftEdgeShape.vertex2, + Vector2(3.9, -66), + ); + + return [ + FixtureDef(leftEdgeShape), + FixtureDef(bottomEdgeShape), + ]; + } + + @override + Body createBody() { + final body = world.createBody(BodyDef()); + _createFixtureDefs().forEach(body.createFixture); + return body; + } +} + +class _FlapperExit extends LayerSensor { + _FlapperExit() + : super( + insideLayer: Layer.launcher, + outsideLayer: Layer.board, + orientation: LayerEntranceOrientation.down, + insideZIndex: ZIndexes.ballOnLaunchRamp, + outsideZIndex: ZIndexes.ballOnBoard, + ) { + layer = Layer.launcher; + } + + @override + Shape get shape => PolygonShape() + ..setAsBox( + 1.7, + 0.1, + initialPosition, + 1.5708, + ); +} + +/// {@template flap_sprite_animation_component} +/// Flap suspended between supports that animates to let the [Ball] exit the +/// [LaunchRamp]. +/// {@endtemplate} +@visibleForTesting +class FlapSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef, ZIndex { + /// {@macro flap_sprite_animation_component} + FlapSpriteAnimationComponent() + : super( + anchor: Anchor.center, + position: Vector2(2.8, -70.7), + playing: false, + ) { + zIndex = ZIndexes.flapper; + } + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.flapper.flap.keyName, + ); + + const amountPerRow = 14; + const amountPerColumn = 1; + 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, + ), + )..onComplete = () { + animation?.reset(); + playing = false; + }; + } +} + +class _BackSupportSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _BackSupportSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(2.95, -70.6), + ) { + zIndex = ZIndexes.flapperBack; + } + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.flapper.backSupport.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} + +class _FrontSupportSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _FrontSupportSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(2.9, -67.6), + ) { + zIndex = ZIndexes.flapperFront; + } + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.flapper.frontSupport.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index 4713c3a2..7dcc274e 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -1,7 +1,5 @@ // ignore_for_file: avoid_renaming_method_parameters -import 'dart:math' as math; - import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -17,8 +15,6 @@ class LaunchRamp extends Component { children: [ _LaunchRampBase(), _LaunchRampForegroundRailing(), - _LaunchRampExit()..initialPosition = Vector2(0.6, -34), - _LaunchRampCloseWall()..initialPosition = Vector2(4, -69.5), ], ); } @@ -109,8 +105,10 @@ class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.launchRamp.ramp.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.launchRamp.ramp.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; @@ -125,8 +123,10 @@ class _LaunchRampBackgroundRailingSpriteComponent extends SpriteComponent Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.launchRamp.backgroundRailing.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.launchRamp.backgroundRailing.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; @@ -190,8 +190,10 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.launchRamp.foregroundRailing.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.launchRamp.foregroundRailing.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; @@ -199,51 +201,3 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent position = Vector2(22.8, 0.5); } } - -class _LaunchRampCloseWall extends BodyComponent with InitialPosition, Layered { - _LaunchRampCloseWall() : super(renderBody: false) { - layer = Layer.board; - } - - @override - Body createBody() { - final shape = EdgeShape()..set(Vector2.zero(), Vector2(0, 3)); - - final fixtureDef = FixtureDef(shape); - - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -/// {@template launch_ramp_exit} -/// [LayerSensor] with [Layer.launcher] to filter [Ball]s exiting the -/// [LaunchRamp]. -/// {@endtemplate} -class _LaunchRampExit extends LayerSensor { - /// {@macro launch_ramp_exit} - _LaunchRampExit() - : super( - insideLayer: Layer.launcher, - outsideLayer: Layer.board, - orientation: LayerEntranceOrientation.down, - insideZIndex: ZIndexes.ballOnLaunchRamp, - outsideZIndex: ZIndexes.ballOnBoard, - ) { - layer = Layer.launcher; - } - - static final Vector2 _size = Vector2(1.6, 0.1); - - @override - Shape get shape => PolygonShape() - ..setAsBox( - _size.x, - _size.y, - initialPosition, - math.pi / 2, - ); -} diff --git a/packages/pinball_components/lib/src/components/z_indexes.dart b/packages/pinball_components/lib/src/components/z_indexes.dart index b8371273..a04402b5 100644 --- a/packages/pinball_components/lib/src/components/z_indexes.dart +++ b/packages/pinball_components/lib/src/components/z_indexes.dart @@ -45,6 +45,12 @@ abstract class ZIndexes { static const launchRampForegroundRailing = _above + ballOnLaunchRamp; + static const flapperBack = _above + outerBoundary; + + static const flapperFront = _above + flapper; + + static const flapper = _above + ballOnLaunchRamp; + static const plunger = _above + launchRamp; static const rocket = _below + bottomBoundary; diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 61e62386..9716a526 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -88,6 +88,7 @@ flutter: - assets/images/multiplier/x5/ - assets/images/multiplier/x6/ - assets/images/score/ + - assets/images/flapper/ flutter_gen: line_length: 80 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 7f07de97..a66459a6 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 @@ -13,8 +13,9 @@ class BallBoosterGame extends LineGame { @override void onLine(Vector2 line) { final ball = Ball(baseColor: Colors.transparent); - add(ball); + final impulse = line * -1 * 20; + ball.add(BallTurboChargingBehavior(impulse: impulse)); - ball.mounted.then((value) => ball.boost(line * -1 * 20)); + add(ball); } } diff --git a/packages/pinball_components/test/src/components/ball/ball_test.dart b/packages/pinball_components/test/src/components/ball/ball_test.dart index 02175f16..655836a0 100644 --- a/packages/pinball_components/test/src/components/ball/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball/ball_test.dart @@ -1,12 +1,10 @@ // ignore_for_file: cascade_invocations -import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.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 'package:pinball_components/src/components/ball/behaviors/behaviors.dart'; import '../../../helpers/helpers.dart'; @@ -180,50 +178,5 @@ void main() { ); }); }); - - group('boost', () { - flameTester.test('applies an impulse to the ball', (game) async { - final ball = Ball(baseColor: baseColor); - await game.ensureAdd(ball); - - expect(ball.body.linearVelocity, equals(Vector2.zero())); - - await ball.boost(Vector2.all(10)); - expect(ball.body.linearVelocity.x, greaterThan(0)); - expect(ball.body.linearVelocity.y, greaterThan(0)); - }); - - flameTester.test('adds TurboChargeSpriteAnimation', (game) async { - final ball = Ball(baseColor: baseColor); - await game.ensureAdd(ball); - - await ball.boost(Vector2.all(10)); - game.update(0); - - expect( - ball.children.whereType().single, - isNotNull, - ); - }); - - flameTester.test('removes TurboChargeSpriteAnimation after it finishes', - (game) async { - final ball = Ball(baseColor: baseColor); - await game.ensureAdd(ball); - - await ball.boost(Vector2.all(10)); - game.update(0); - - final turboChargeSpriteAnimation = - ball.children.whereType().single; - - expect(ball.contains(turboChargeSpriteAnimation), isTrue); - - game.update(turboChargeSpriteAnimation.animation!.totalDuration()); - game.update(0.1); - - expect(ball.contains(turboChargeSpriteAnimation), isFalse); - }); - }); }); } diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart index de291f21..d78df37a 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart @@ -6,7 +6,6 @@ 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 'package:pinball_components/src/components/ball/behaviors/behaviors.dart'; import '../../../../helpers/helpers.dart'; diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart index cd0a0486..0aeeda98 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -6,7 +6,6 @@ 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 'package:pinball_components/src/components/ball/behaviors/behaviors.dart'; import '../../../../helpers/helpers.dart'; diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart new file mode 100644 index 00000000..00f34832 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart @@ -0,0 +1,94 @@ +// ignore_for_file: cascade_invocations + +import 'dart:ui'; + +import 'package:flame/components.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() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'BallTurboChargingBehavior', + () { + final assets = [Assets.images.ball.ball.keyName]; + final flameTester = FlameTester(() => TestGame(assets)); + const baseColor = Color(0xFFFFFFFF); + + test('can be instantiated', () { + expect( + BallTurboChargingBehavior(impulse: Vector2.zero()), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final ball = Ball.test(baseColor: baseColor); + final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); + await ball.add(behavior); + await game.ensureAdd(ball); + expect( + ball.firstChild(), + equals(behavior), + ); + }); + + flameTester.test( + 'impulses the ball velocity when loaded', + (game) async { + final ball = Ball.test(baseColor: baseColor); + await game.ensureAdd(ball); + final impulse = Vector2.all(1); + final behavior = BallTurboChargingBehavior(impulse: impulse); + await ball.ensureAdd(behavior); + + expect( + ball.body.linearVelocity.x, + equals(impulse.x), + ); + expect( + ball.body.linearVelocity.y, + equals(impulse.y), + ); + }, + ); + + flameTester.test('adds sprite', (game) async { + final ball = Ball(baseColor: baseColor); + await game.ensureAdd(ball); + + await ball.ensureAdd( + BallTurboChargingBehavior(impulse: Vector2.zero()), + ); + + expect( + ball.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('removes sprite after it finishes', (game) async { + final ball = Ball(baseColor: baseColor); + await game.ensureAdd(ball); + + final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); + await ball.ensureAdd(behavior); + + final turboChargeSpriteAnimation = + ball.children.whereType().single; + + expect(ball.contains(turboChargeSpriteAnimation), isTrue); + + game.update(behavior.timer.limit); + game.update(0.1); + + expect(ball.contains(turboChargeSpriteAnimation), isFalse); + }); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/flapper/behaviors/flapper_spinning_behavior_test.dart b/packages/pinball_components/test/src/components/flapper/behaviors/flapper_spinning_behavior_test.dart new file mode 100644 index 00000000..c53dc17b --- /dev/null +++ b/packages/pinball_components/test/src/components/flapper/behaviors/flapper_spinning_behavior_test.dart @@ -0,0 +1,53 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/flapper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.flapper.flap.keyName, + Assets.images.flapper.backSupport.keyName, + Assets.images.flapper.frontSupport.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group( + 'FlapperSpinningBehavior', + () { + test('can be instantiated', () { + expect( + FlapperSpinningBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact plays the flapper animation', + (game) async { + final behavior = FlapperSpinningBehavior(); + final entrance = FlapperEntrance(); + final flap = FlapSpriteAnimationComponent(); + final flapper = Flapper.test(); + await flapper.addAll([entrance, flap]); + await entrance.add(behavior); + await game.ensureAdd(flapper); + + behavior.beginContact(_MockBall(), _MockContact()); + + expect(flap.playing, isTrue); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/flapper/flapper_test.dart b/packages/pinball_components/test/src/components/flapper/flapper_test.dart new file mode 100644 index 00000000..497bb5f6 --- /dev/null +++ b/packages/pinball_components/test/src/components/flapper/flapper_test.dart @@ -0,0 +1,100 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/flapper/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + group('Flapper', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.flapper.flap.keyName, + Assets.images.flapper.backSupport.keyName, + Assets.images.flapper.frontSupport.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('loads correctly', (game) async { + final component = Flapper(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final canvas = ZCanvasComponent(children: [Flapper()]); + await game.ensureAdd(canvas); + game.camera + ..followVector2(Vector2(3, -70)) + ..zoom = 25; + await tester.pump(); + }, + verify: (game, tester) async { + const goldenFilePath = '../golden/flapper/'; + final flapSpriteAnimationComponent = game + .descendants() + .whereType() + .first + ..playing = true; + final animationDuration = + flapSpriteAnimationComponent.animation!.totalDuration(); + + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenFilePath}start.png'), + ); + + game.update(animationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenFilePath}middle.png'), + ); + + game.update(animationDuration * 0.75); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenFilePath}end.png'), + ); + }, + ); + + flameTester.test('adds a FlapperSpiningBehavior to FlapperEntrance', + (game) async { + final flapper = Flapper(); + await game.ensureAdd(flapper); + + final flapperEntrance = flapper.firstChild()!; + expect( + flapperEntrance.firstChild(), + isNotNull, + ); + }); + + flameTester.test( + 'flap stops animating after animation completes', + (game) async { + final flapper = Flapper(); + await game.ensureAdd(flapper); + + final flapSpriteAnimationComponent = + flapper.firstChild()!; + + flapSpriteAnimationComponent.playing = true; + game.update( + flapSpriteAnimationComponent.animation!.totalDuration() + 0.1, + ); + + expect(flapSpriteAnimationComponent.playing, isFalse); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/golden/flapper/end.png b/packages/pinball_components/test/src/components/golden/flapper/end.png new file mode 100644 index 00000000..31319b37 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/flapper/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/flapper/middle.png b/packages/pinball_components/test/src/components/golden/flapper/middle.png new file mode 100644 index 00000000..4f0484f3 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/flapper/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/flapper/start.png b/packages/pinball_components/test/src/components/golden/flapper/start.png new file mode 100644 index 00000000..e6da466a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/flapper/start.png differ diff --git a/packages/pinball_components/test/src/components/launch_ramp_test.dart b/packages/pinball_components/test/src/components/launch_ramp_test.dart index 44fa8609..38c0920b 100644 --- a/packages/pinball_components/test/src/components/launch_ramp_test.dart +++ b/packages/pinball_components/test/src/components/launch_ramp_test.dart @@ -9,7 +9,13 @@ import '../../helpers/helpers.dart'; void main() { group('LaunchRamp', () { - final flameTester = FlameTester(TestGame.new); + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.launchRamp.ramp.keyName, + Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.launchRamp.foregroundRailing.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); flameTester.test('loads correctly', (game) async { final component = LaunchRamp(); @@ -20,9 +26,12 @@ void main() { flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { + await game.images.loadAll(assets); await game.ensureAdd(LaunchRamp()); game.camera.followVector2(Vector2.zero()); game.camera.zoom = 4.1; + await game.ready(); + await tester.pump(); }, verify: (game, tester) async { await expectLater( diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index cfb3e157..d8d31b4e 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: cascade_invocations import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -99,6 +100,7 @@ void main() { group('turboCharge', () { setUpAll(() { registerFallbackValue(Vector2.zero()); + registerFallbackValue(Component()); }); flameBlocTester.testGameWidget( @@ -124,7 +126,7 @@ void main() { final controller = _WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); - when(() => ball.boost(any())).thenAnswer((_) async {}); + when(() => ball.add(any())).thenAnswer((_) async {}); await controller.turboCharge(); @@ -140,29 +142,13 @@ void main() { final controller = _WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); - when(() => ball.boost(any())).thenAnswer((_) async {}); + when(() => ball.add(any())).thenAnswer((_) async {}); await controller.turboCharge(); verify(ball.resume).called(1); }, ); - - flameBlocTester.test( - 'boosts the ball', - (game) async { - final gameRef = _MockPinballGame(); - final ball = _MockControlledBall(); - final controller = _WrappedBallController(ball, gameRef); - when(() => gameRef.read()).thenReturn(gameBloc); - when(() => ball.controller).thenReturn(controller); - when(() => ball.boost(any())).thenAnswer((_) async {}); - - await controller.turboCharge(); - - verify(() => ball.boost(any())).called(1); - }, - ); }); }); } diff --git a/test/game/components/launcher_test.dart b/test/game/components/launcher_test.dart new file mode 100644 index 00000000..c76e6b7e --- /dev/null +++ b/test/game/components/launcher_test.dart @@ -0,0 +1,85 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.launchRamp.ramp.keyName, + Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.launchRamp.foregroundRailing.keyName, + Assets.images.flapper.backSupport.keyName, + Assets.images.flapper.frontSupport.keyName, + Assets.images.flapper.flap.keyName, + Assets.images.plunger.plunger.keyName, + Assets.images.plunger.rocket.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('Launcher', () { + flameTester.test( + 'loads correctly', + (game) async { + final launcher = Launcher(); + await game.ensureAdd(launcher); + + expect(game.contains(launcher), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'a LaunchRamp', + (game) async { + final launcher = Launcher(); + await game.ensureAdd(launcher); + + final descendantsQuery = + launcher.descendants().whereType(); + expect(descendantsQuery.length, equals(1)); + }, + ); + + flameTester.test( + 'a Flapper', + (game) async { + final launcher = Launcher(); + await game.ensureAdd(launcher); + + final descendantsQuery = launcher.descendants().whereType(); + expect(descendantsQuery.length, equals(1)); + }, + ); + + flameTester.test( + 'a Plunger', + (game) async { + final launcher = Launcher(); + await game.ensureAdd(launcher); + + final descendantsQuery = launcher.descendants().whereType(); + expect(descendantsQuery.length, equals(1)); + }, + ); + + flameTester.test( + 'a RocketSpriteComponent', + (game) async { + final launcher = Launcher(); + await game.ensureAdd(launcher); + + final descendantsQuery = + launcher.descendants().whereType(); + expect(descendantsQuery.length, equals(1)); + }, + ); + }); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 10203175..6bf40129 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -124,6 +124,9 @@ void main() { Assets.images.sparky.bumper.b.dimmed.keyName, Assets.images.sparky.bumper.c.lit.keyName, Assets.images.sparky.bumper.c.dimmed.keyName, + Assets.images.flapper.flap.keyName, + Assets.images.flapper.backSupport.keyName, + Assets.images.flapper.frontSupport.keyName, ]; late GameBloc gameBloc; diff --git a/web/index.html b/web/index.html index 107b34ba..f60ae7ce 100644 --- a/web/index.html +++ b/web/index.html @@ -26,14 +26,13 @@ - - + + content="https://firebasestorage.googleapis.com/v0/b/io-pinball.appspot.com/o/images%2Fpinball_share_image.png?alt=media"> + content="https://firebasestorage.googleapis.com/v0/b/io-pinball.appspot.com/o/images%2Fpinball_share_image.png?alt=media">