diff --git a/packages/pinball_components/assets/images/sparky_bumper/a/active.png b/packages/pinball_components/assets/images/sparky_bumper/a/active.png new file mode 100644 index 00000000..6e84d8ef Binary files /dev/null and b/packages/pinball_components/assets/images/sparky_bumper/a/active.png differ diff --git a/packages/pinball_components/assets/images/sparky_bumper/a/inactive.png b/packages/pinball_components/assets/images/sparky_bumper/a/inactive.png new file mode 100644 index 00000000..9157a73f Binary files /dev/null and b/packages/pinball_components/assets/images/sparky_bumper/a/inactive.png differ diff --git a/packages/pinball_components/assets/images/sparky_bumper/b/active.png b/packages/pinball_components/assets/images/sparky_bumper/b/active.png new file mode 100644 index 00000000..02371ce1 Binary files /dev/null and b/packages/pinball_components/assets/images/sparky_bumper/b/active.png differ diff --git a/packages/pinball_components/assets/images/sparky_bumper/b/inactive.png b/packages/pinball_components/assets/images/sparky_bumper/b/inactive.png new file mode 100644 index 00000000..20b4f092 Binary files /dev/null and b/packages/pinball_components/assets/images/sparky_bumper/b/inactive.png differ diff --git a/packages/pinball_components/assets/images/sparky_bumper/c/active.png b/packages/pinball_components/assets/images/sparky_bumper/c/active.png new file mode 100644 index 00000000..85748375 Binary files /dev/null and b/packages/pinball_components/assets/images/sparky_bumper/c/active.png differ diff --git a/packages/pinball_components/assets/images/sparky_bumper/c/inactive.png b/packages/pinball_components/assets/images/sparky_bumper/c/inactive.png new file mode 100644 index 00000000..b5b3584d Binary files /dev/null and b/packages/pinball_components/assets/images/sparky_bumper/c/inactive.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 154930bc..de59219e 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -30,6 +30,8 @@ class $AssetsImagesGen { $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); + $AssetsImagesSparkyBumperGen get sparkyBumper => + const $AssetsImagesSparkyBumperGen(); } class $AssetsImagesBaseboardGen { @@ -142,6 +144,14 @@ class $AssetsImagesSpaceshipGen { const AssetGenImage('assets/images/spaceship/saucer.png'); } +class $AssetsImagesSparkyBumperGen { + const $AssetsImagesSparkyBumperGen(); + + $AssetsImagesSparkyBumperAGen get a => const $AssetsImagesSparkyBumperAGen(); + $AssetsImagesSparkyBumperBGen get b => const $AssetsImagesSparkyBumperBGen(); + $AssetsImagesSparkyBumperCGen get c => const $AssetsImagesSparkyBumperCGen(); +} + class $AssetsImagesDashBumperAGen { const $AssetsImagesDashBumperAGen(); @@ -206,6 +216,42 @@ class $AssetsImagesSpaceshipRampGen { 'assets/images/spaceship/ramp/railing-foreground.png'); } +class $AssetsImagesSparkyBumperAGen { + const $AssetsImagesSparkyBumperAGen(); + + /// File path: assets/images/sparky_bumper/a/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/sparky_bumper/a/active.png'); + + /// File path: assets/images/sparky_bumper/a/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/sparky_bumper/a/inactive.png'); +} + +class $AssetsImagesSparkyBumperBGen { + const $AssetsImagesSparkyBumperBGen(); + + /// File path: assets/images/sparky_bumper/b/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/sparky_bumper/b/active.png'); + + /// File path: assets/images/sparky_bumper/b/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/sparky_bumper/b/inactive.png'); +} + +class $AssetsImagesSparkyBumperCGen { + const $AssetsImagesSparkyBumperCGen(); + + /// File path: assets/images/sparky_bumper/c/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/sparky_bumper/c/active.png'); + + /// File path: assets/images/sparky_bumper/c/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/sparky_bumper/c/inactive.png'); +} + class Assets { Assets._(); diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index bf578ea7..6b0c2ef5 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -19,3 +19,4 @@ export 'shapes/shapes.dart'; export 'spaceship.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; +export 'sparky_bumper.dart'; diff --git a/packages/pinball_components/lib/src/components/sparky_bumper.dart b/packages/pinball_components/lib/src/components/sparky_bumper.dart new file mode 100644 index 00000000..e6a5f237 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper.dart @@ -0,0 +1,125 @@ +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 sparky_bumper} +/// Bumper for Sparky area. +/// {@endtemplate} +// TODO(ruimiguel): refactor later to unify with DashBumpers. +class SparkyBumper extends BodyComponent with InitialPosition { + /// {@macro sparky_bumper} + SparkyBumper._({ + 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 sparky_bumper} + SparkyBumper.a() + : this._( + majorRadius: 2.9, + minorRadius: 2.1, + activeAssetPath: Assets.images.sparkyBumper.a.active.keyName, + inactiveAssetPath: Assets.images.sparkyBumper.a.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0, -0.25), + ), + ); + + /// {@macro sparky_bumper} + SparkyBumper.b() + : this._( + majorRadius: 2.85, + minorRadius: 2, + activeAssetPath: Assets.images.sparkyBumper.b.active.keyName, + inactiveAssetPath: Assets.images.sparkyBumper.b.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0, -0.35), + ), + ); + + /// {@macro sparky_bumper} + SparkyBumper.c() + : this._( + majorRadius: 3, + minorRadius: 2.2, + activeAssetPath: Assets.images.sparkyBumper.c.active.keyName, + inactiveAssetPath: Assets.images.sparkyBumper.c.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0, -0.4), + ), + ); + + 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(); + await _loadSprites(); + + // TODO(erickzanardo): Look into using onNewState instead. + // Currently doing: onNewState(gameRef.read()) will throw an + // `Exception: build context is not available yet` + deactivate(); + await add(_spriteComponent); + } + + @override + Body createBody() { + renderBody = false; + + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: _majorRadius, + minorRadius: _minorRadius, + )..rotate(math.pi / 1.9); + 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 [DashNestBumper]. + void activate() { + _spriteComponent + ..sprite = _activeSprite + ..size = _activeSprite.originalSize / 10; + } + + /// Deactivates the [DashNestBumper]. + void deactivate() { + _spriteComponent + ..sprite = _inactiveSprite + ..size = _inactiveSprite.originalSize / 10; + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index c7302d0d..312e01f3 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -39,6 +39,9 @@ flutter: - assets/images/spaceship/ramp/ - assets/images/chrome_dino/ - assets/images/kicker/ + - assets/images/sparky_bumper/a/ + - assets/images/sparky_bumper/b/ + - assets/images/sparky_bumper/c/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 1801fa52..88b86da6 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -21,5 +21,6 @@ void main() { addChromeDinoStories(dashbook); addDashNestBumperStories(dashbook); addKickerStories(dashbook); + addSparkyBumperStories(dashbook); runApp(dashbook); } 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 new file mode 100644 index 00000000..a0ad661a --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SparkyBumperGame extends BasicBallGame { + SparkyBumperGame({ + required this.trace, + }) : super(color: const Color(0xFF0000FF)); + + static const info = ''' + Shows how a SparkyBumper is rendered. + + Activate the "trace" parameter to overlay the body. +'''; + + final bool trace; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + final sparkyBumperA = SparkyBumper.a() + ..initialPosition = Vector2(center.x - 20, center.y - 20) + ..priority = 1; + final sparkyBumperB = SparkyBumper.b() + ..initialPosition = Vector2(center.x - 10, center.y + 10) + ..priority = 1; + final sparkyBumperC = SparkyBumper.c() + ..initialPosition = Vector2(center.x + 20, center.y) + ..priority = 1; + await addAll([ + sparkyBumperA, + sparkyBumperB, + sparkyBumperC, + ]); + + if (trace) { + sparkyBumperA.trace(); + sparkyBumperB.trace(); + sparkyBumperC.trace(); + } + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart new file mode 100644 index 00000000..d0933b67 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart @@ -0,0 +1,17 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/sparky_bumper/sparky_bumper_game.dart'; + +void addSparkyBumperStories(Dashbook dashbook) { + dashbook.storiesOf('Sparky Bumpers').add( + 'Basic', + (context) => GameWidget( + game: SparkyBumperGame( + trace: context.boolProperty('Trace', true), + ), + ), + codeLink: buildSourceLink('sparky_bumper/basic.dart'), + info: SparkyBumperGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 108cca05..746d83d6 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -6,3 +6,4 @@ export 'effects/stories.dart'; export 'flipper/stories.dart'; export 'layer/stories.dart'; export 'spaceship/stories.dart'; +export 'sparky_bumper/stories.dart'; diff --git a/packages/pinball_components/test/src/components/sparky_bumper_test.dart b/packages/pinball_components/test/src/components/sparky_bumper_test.dart new file mode 100644 index 00000000..470c254b --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper_test.dart @@ -0,0 +1,74 @@ +// 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('SparkyBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final bumper = SparkyBumper.a(); + await game.ensureAdd(bumper); + + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final bumper = SparkyBumper.b(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"c" loads correctly', (game) async { + final bumper = SparkyBumper.c(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('activate returns normally', (game) async { + final bumper = SparkyBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.activate, returnsNormally); + }); + + flameTester.test('deactivate returns normally', (game) async { + final bumper = SparkyBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.deactivate, returnsNormally); + }); + + flameTester.test('changes sprite', (game) async { + final bumper = SparkyBumper.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)), + ); + }); + }); +}