diff --git a/.github/workflows/share_repository.yaml b/.github/workflows/share_repository.yaml new file mode 100644 index 00000000..2860902b --- /dev/null +++ b/.github/workflows/share_repository.yaml @@ -0,0 +1,18 @@ +name: share_repository + +on: + push: + paths: + - "packages/share_repository/**" + - ".github/workflows/share_repository.yaml" + + pull_request: + paths: + - "packages/share_repository/**" + - ".github/workflows/share_repository.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/share_repository diff --git a/assets/images/bonus_animation/android.png b/assets/images/bonus_animation/android_spaceship.png similarity index 100% rename from assets/images/bonus_animation/android.png rename to assets/images/bonus_animation/android_spaceship.png diff --git a/assets/images/bonus_animation/dino.png b/assets/images/bonus_animation/dino_chomp.png similarity index 100% rename from assets/images/bonus_animation/dino.png rename to assets/images/bonus_animation/dino_chomp.png diff --git a/assets/images/bonus_animation/google.png b/assets/images/bonus_animation/google_word.png similarity index 100% rename from assets/images/bonus_animation/google.png rename to assets/images/bonus_animation/google_word.png diff --git a/assets/images/score/mini_score_background.png b/assets/images/score/mini_score_background.png new file mode 100644 index 00000000..781f7349 Binary files /dev/null and b/assets/images/score/mini_score_background.png differ diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 2780b608..97cfec9b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -13,7 +13,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; class App extends StatelessWidget { @@ -36,7 +36,7 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _pinballAudio), ], child: BlocProvider( - create: (context) => ThemeCubit(), + create: (context) => CharacterThemeCubit(), child: const MaterialApp( title: 'I/O Pinball', localizationsDelegates: [ diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index c57eedb4..772bfea3 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -12,6 +12,12 @@ enum GameBonus { /// Bonus achieved when a ball enters Sparky's computer. sparkyTurboCharge, + + /// Bonus achieved when the ball goes in the dino mouth. + dinoChomp, + + /// Bonus achieved when a ball enters the android spaceship. + androidSpaceship, } /// {@template game_state} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index fc914ebf..7d4b23f7 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -4,7 +4,6 @@ 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 'google_word.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 6b983cea..6468c821 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -15,8 +15,8 @@ class ControlledBall extends Ball with Controls { /// When a launched [Ball] is lost, it will decrease the [GameState.balls] /// count, and a new [Ball] is spawned. ControlledBall.launch({ - required PinballTheme theme, - }) : super(baseColor: theme.characterTheme.ballColor) { + required CharacterTheme characterTheme, + }) : super(baseColor: characterTheme.ballColor) { controller = BallController(this); priority = RenderPriority.ballOnLaunchRamp; layer = Layer.launcher; @@ -28,8 +28,8 @@ class ControlledBall extends Ball with Controls { /// When a bonus [Ball] is lost, the [GameState.balls] doesn't change. /// {@endtemplate} ControlledBall.bonus({ - required PinballTheme theme, - }) : super(baseColor: theme.characterTheme.ballColor) { + required CharacterTheme characterTheme, + }) : super(baseColor: characterTheme.ballColor) { controller = BallController(this); priority = RenderPriority.ballOnBoard; } @@ -62,10 +62,13 @@ class BallController extends ComponentController Future turboCharge() async { gameRef.read().add(const SparkyTurboChargeActivated()); - // TODO(allisonryan0002): adjust delay to match animation duration once - // given animations. component.stop(); - await Future.delayed(const Duration(seconds: 1)); + // TODO(alestiago): Refactor this hard coded duration once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1564 + await Future.delayed( + const Duration(milliseconds: 2583), + ); component.resume(); await component.boost(Vector2(40, 110)); } diff --git a/lib/game/components/controlled_sparky_computer.dart b/lib/game/components/controlled_sparky_computer.dart deleted file mode 100644 index 65190a1f..00000000 --- a/lib/game/components/controlled_sparky_computer.dart +++ /dev/null @@ -1,52 +0,0 @@ -// 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/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template controlled_sparky_computer} -/// [SparkyComputer] with a [SparkyComputerController] attached. -/// {@endtemplate} -class ControlledSparkyComputer extends SparkyComputer - with Controls, HasGameRef { - /// {@macro controlled_sparky_computer} - ControlledSparkyComputer() : super() { - controller = SparkyComputerController(this); - } - - @override - Future onLoad() async { - await super.onLoad(); - gameRef.addContactCallback(SparkyComputerSensorBallContactCallback()); - } -} - -/// {@template sparky_computer_controller} -/// Controller attached to a [SparkyComputer] that handles its game related -/// logic. -/// {@endtemplate} -// TODO(allisonryan0002): listen for turbo charge game bonus and animate Sparky. -class SparkyComputerController - extends ComponentController { - /// {@macro sparky_computer_controller} - SparkyComputerController(ControlledSparkyComputer controlledComputer) - : super(controlledComputer); -} - -/// {@template sparky_computer_sensor_ball_contact_callback} -/// Turbo charges the [Ball] when it enters the [SparkyComputer] -/// {@endtemplate} -@visibleForTesting -class SparkyComputerSensorBallContactCallback - extends ContactCallback { - /// {@macro sparky_computer_sensor_ball_contact_callback} - SparkyComputerSensorBallContactCallback(); - - @override - void begin(_, ControlledBall ball, __) { - ball.controller.turboCharge(); - } -} diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart index 9c8ab309..971cd700 100644 --- a/lib/game/components/flutter_forest.dart +++ b/lib/game/components/flutter_forest.dart @@ -71,7 +71,7 @@ class _FlutterForestController extends ComponentController Future _addBonusBall() async { await gameRef.add( - ControlledBall.bonus(theme: gameRef.theme) + ControlledBall.bonus(characterTheme: gameRef.characterTheme) ..initialPosition = Vector2(17.2, -52.7), ); } diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index 77afebe6..48dd5518 100644 --- a/lib/game/components/game_flow_controller.dart +++ b/lib/game/components/game_flow_controller.dart @@ -32,8 +32,7 @@ class GameFlowController extends ComponentController // next page component.firstChild()?.gameOverMode( score: state?.score ?? 0, - characterIconPath: - component.theme.characterTheme.leaderboardIcon.keyName, + characterIconPath: component.characterTheme.leaderboardIcon.keyName, ); component.firstChild()?.focusOnBackboard(); } diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart index 6d78d32b..a5450761 100644 --- a/lib/game/components/sparky_fire_zone.dart +++ b/lib/game/components/sparky_fire_zone.dart @@ -1,10 +1,10 @@ // 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/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_fire_zone} /// Area positioned at the top left of the [Board] where the [Ball] @@ -12,29 +12,21 @@ import 'package:pinball_components/pinball_components.dart'; /// /// When a [Ball] hits [SparkyBumper]s, the bumper animates. /// {@endtemplate} -class SparkyFireZone extends Component with HasGameRef { +class SparkyFireZone extends Blueprint { /// {@macro sparky_fire_zone} - SparkyFireZone(); - - @override - Future onLoad() async { - await super.onLoad(); - - gameRef.addContactCallback(SparkyBumperBallContactCallback()); - - final lowerLeftBumper = _SparkyBumper.a() - ..initialPosition = Vector2(-22.9, -41.65); - final upperLeftBumper = _SparkyBumper.b() - ..initialPosition = Vector2(-21.25, -57.9); - final rightBumper = _SparkyBumper.c() - ..initialPosition = Vector2(-3.3, -52.55); - - await addAll([ - lowerLeftBumper, - upperLeftBumper, - rightBumper, - ]); - } + SparkyFireZone() + : super( + components: [ + _SparkyBumper.a()..initialPosition = Vector2(-22.9, -41.65), + _SparkyBumper.b()..initialPosition = Vector2(-21.25, -57.9), + _SparkyBumper.c()..initialPosition = Vector2(-3.3, -52.55), + SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8), + SparkyAnimatronic()..position = Vector2(-13.8, -58.2), + ], + blueprints: [ + SparkyComputer(), + ], + ); } // TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D @@ -48,6 +40,14 @@ class _SparkyBumper extends SparkyBumper with ScorePoints { @override int get points => 20; + + @override + Future onLoad() async { + await super.onLoad(); + // TODO(alestiago): Revisit once this has been merged: + // https://github.com/flame-engine/flame/pull/1547 + gameRef.addContactCallback(SparkyBumperBallContactCallback()); + } } /// Listens when a [Ball] bounces bounces against a [SparkyBumper]. @@ -63,3 +63,48 @@ class SparkyBumperBallContactCallback sparkyBumper.animate(); } } + +/// {@template sparky_computer_sensor} +/// Small sensor body used to detect when a ball has entered the +/// [SparkyComputer]. +/// {@endtemplate} +// TODO(alestiago): Revisit once this has been merged: +// https://github.com/flame-engine/flame/pull/1547 +class SparkyComputerSensor extends BodyComponent with InitialPosition { + /// {@macro sparky_computer_sensor} + SparkyComputerSensor() { + renderBody = false; + } + + @override + Body createBody() { + final shape = CircleShape()..radius = 0.1; + final fixtureDef = FixtureDef(shape, isSensor: true); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + + @override + Future onLoad() async { + await super.onLoad(); + // TODO(alestiago): Revisit once this has been merged: + // https://github.com/flame-engine/flame/pull/1547 + gameRef.addContactCallback(SparkyComputerSensorBallContactCallback()); + } +} + +@visibleForTesting +// TODO(alestiago): Revisit once this has been merged: +// https://github.com/flame-engine/flame/pull/1547 +// ignore: public_member_api_docs +class SparkyComputerSensorBallContactCallback + extends ContactCallback { + @override + void begin(_, ControlledBall controlledBall, __) { + controlledBall.controller.turboCharge(); + controlledBall.gameRef.firstChild()?.playing = true; + } +} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 1cd4f194..9dc88562 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -43,8 +43,11 @@ extension PinballGameAssetsX on PinballGame { 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.plunger.plunger.keyName), + images.load(components.Assets.images.plunger.rocket.keyName), images.load(components.Assets.images.boundary.bottom.keyName), images.load(components.Assets.images.boundary.outer.keyName), + images.load(components.Assets.images.boundary.outerBottom.keyName), images.load(components.Assets.images.spaceship.saucer.keyName), images.load(components.Assets.images.spaceship.bridge.keyName), images.load(components.Assets.images.spaceship.ramp.boardOpening.keyName), @@ -80,12 +83,11 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.alienBumper.b.inactive.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.plunger.rocket.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), + images.load(components.Assets.images.sparky.computer.base.keyName), + images.load(components.Assets.images.sparky.animatronic.keyName), images.load(components.Assets.images.sparky.bumper.a.inactive.keyName), + images.load(components.Assets.images.sparky.bumper.a.active.keyName), images.load(components.Assets.images.sparky.bumper.b.active.keyName), images.load(components.Assets.images.sparky.bumper.b.inactive.keyName), images.load(components.Assets.images.sparky.bumper.c.active.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index d73fb33c..374a55a1 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; @@ -19,7 +20,7 @@ class PinballGame extends Forge2DGame HasKeyboardHandlerComponents, Controls<_GameBallsController> { PinballGame({ - required this.theme, + required this.characterTheme, required this.audio, }) { images.prefix = ''; @@ -32,7 +33,7 @@ class PinballGame extends Forge2DGame @override Color backgroundColor() => Colors.transparent; - final PinballTheme theme; + final CharacterTheme characterTheme; final PinballAudio audio; @@ -49,13 +50,13 @@ class PinballGame extends Forge2DGame // TODO(allisonryan0002): banish Wall and Board classes in later PR. await add(BottomWall()); unawaited(addFromBlueprint(Boundaries())); - unawaited(addFromBlueprint(ControlledSparkyComputer())); + unawaited(addFromBlueprint(LaunchRamp())); final launcher = Launcher(); unawaited(addFromBlueprint(launcher)); unawaited(add(Board())); unawaited(add(AlienZone())); - unawaited(add(SparkyFireZone())); + await addFromBlueprint(SparkyFireZone()); unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(DinoWalls())); unawaited(_addBonusWord()); @@ -118,7 +119,7 @@ class _GameBallsController extends ComponentController void _spawnBall() { final ball = ControlledBall.launch( - theme: gameRef.theme, + characterTheme: gameRef.characterTheme, )..initialPosition = Vector2( _plunger.body.position.x, _plunger.body.position.y - Ball.size.y, @@ -134,12 +135,12 @@ class _GameBallsController extends ComponentController } } -class DebugPinballGame extends PinballGame with TapDetector { +class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { DebugPinballGame({ - required PinballTheme theme, + required CharacterTheme characterTheme, required PinballAudio audio, }) : super( - theme: theme, + characterTheme: characterTheme, audio: audio, ) { controller = _DebugGameBallsController(this); @@ -149,6 +150,7 @@ class DebugPinballGame extends PinballGame with TapDetector { Future onLoad() async { await super.onLoad(); await _loadBackground(); + await add(_DebugInformation()); } // TODO(alestiago): Move to PinballGame once we have the real background @@ -191,3 +193,35 @@ class _DebugGameBallsController extends _GameBallsController { return noBallsLeft && canBallRespawn; } } + +class _DebugInformation extends Component with HasGameRef { + _DebugInformation() : super(priority: RenderPriority.debugInfo); + + @override + PositionType get positionType => PositionType.widget; + + final _debugTextPaint = TextPaint( + style: const TextStyle( + color: Colors.green, + fontSize: 10, + ), + ); + + final _debugBackgroundPaint = Paint()..color = Colors.white; + + @override + void render(Canvas canvas) { + final debugText = [ + 'FPS: ${gameRef.fps().toStringAsFixed(1)}', + 'BALLS: ${gameRef.descendants().whereType().length}', + ].join(' | '); + + final height = _debugTextPaint.measureTextHeight(debugText); + final position = Vector2(0, gameRef.camera.canvasSize.y - height); + canvas.drawRect( + position & Vector2(gameRef.camera.canvasSize.x, height), + _debugBackgroundPaint, + ); + _debugTextPaint.render(canvas, debugText, position); + } +} diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 38ae0144..2515dd19 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -5,8 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; -import 'package:pinball/theme/theme.dart'; import 'package:pinball_audio/pinball_audio.dart'; class PinballGamePage extends StatelessWidget { @@ -31,17 +31,19 @@ class PinballGamePage extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = context.read().state.theme; + final characterTheme = + context.read().state.characterTheme; final audio = context.read(); final pinballAudio = context.read(); final game = isDebugMode - ? DebugPinballGame(theme: theme, audio: audio) - : PinballGame(theme: theme, audio: audio); + ? DebugPinballGame(characterTheme: characterTheme, audio: audio) + : PinballGame(characterTheme: characterTheme, audio: audio); final loadables = [ ...game.preLoadAssets(), pinballAudio.load(), + ...BonusAnimation.loadAssets(), ]; return MultiBlocProvider( @@ -112,6 +114,13 @@ class PinballGameLoadedView extends StatelessWidget { @override Widget build(BuildContext context) { + final isPlaying = context.select( + (StartGameBloc bloc) => bloc.state.status == StartGameStatus.play, + ); + final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; + final screenWidth = MediaQuery.of(context).size.width; + final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); + return Stack( children: [ Positioned.fill( @@ -130,10 +139,13 @@ class PinballGameLoadedView extends StatelessWidget { }, ), ), - const Positioned( - top: 8, - left: 8, - child: GameHud(), + Positioned( + top: 16, + left: leftMargin, + child: Visibility( + visible: isPlaying, + child: const GameHud(), + ), ), ], ); diff --git a/lib/game/view/widgets/bonus_animation.dart b/lib/game/view/widgets/bonus_animation.dart index 39cee913..da67e1aa 100644 --- a/lib/game/view/widgets/bonus_animation.dart +++ b/lib/game/view/widgets/bonus_animation.dart @@ -1,19 +1,23 @@ -// ignore_for_file: public_member_api_docs - import 'package:flame/flame.dart'; import 'package:flame/sprite.dart'; -import 'package:flame/widgets.dart'; import 'package:flutter/material.dart' hide Image; import 'package:pinball/gen/assets.gen.dart'; +import 'package:pinball_flame/pinball_flame.dart'; -class BonusAnimation extends StatelessWidget { +/// {@template bonus_animation} +/// [Widget] that displays bonus animations. +/// {@endtemplate} +class BonusAnimation extends StatefulWidget { + /// {@macro bonus_animation} const BonusAnimation._( - this.imagePath, { + String imagePath, { VoidCallback? onCompleted, Key? key, - }) : _onCompleted = onCompleted, + }) : _imagePath = imagePath, + _onCompleted = onCompleted, super(key: key); + /// [Widget] that displays the dash nest animation. BonusAnimation.dashNest({ Key? key, VoidCallback? onCompleted, @@ -23,6 +27,7 @@ class BonusAnimation extends StatelessWidget { key: key, ); + /// [Widget] that displays the sparky turbo charge animation. BonusAnimation.sparkyTurboCharge({ Key? key, VoidCallback? onCompleted, @@ -32,56 +37,94 @@ class BonusAnimation extends StatelessWidget { key: key, ); - BonusAnimation.dino({ + /// [Widget] that displays the dino chomp animation. + BonusAnimation.dinoChomp({ Key? key, VoidCallback? onCompleted, }) : this._( - Assets.images.bonusAnimation.dino.keyName, + Assets.images.bonusAnimation.dinoChomp.keyName, onCompleted: onCompleted, key: key, ); - BonusAnimation.android({ + /// [Widget] that displays the android spaceship animation. + BonusAnimation.androidSpaceship({ Key? key, VoidCallback? onCompleted, }) : this._( - Assets.images.bonusAnimation.android.keyName, + Assets.images.bonusAnimation.androidSpaceship.keyName, onCompleted: onCompleted, key: key, ); - BonusAnimation.google({ + /// [Widget] that displays the google word animation. + BonusAnimation.googleWord({ Key? key, VoidCallback? onCompleted, }) : this._( - Assets.images.bonusAnimation.google.keyName, + Assets.images.bonusAnimation.googleWord.keyName, onCompleted: onCompleted, key: key, ); - final String imagePath; + final String _imagePath; final VoidCallback? _onCompleted; - static Future loadAssets() { + /// Returns a list of assets to be loaded for animations. + static List loadAssets() { Flame.images.prefix = ''; - return Flame.images.loadAll([ - Assets.images.bonusAnimation.dashNest.keyName, - Assets.images.bonusAnimation.sparkyTurboCharge.keyName, - Assets.images.bonusAnimation.dino.keyName, - Assets.images.bonusAnimation.android.keyName, - Assets.images.bonusAnimation.google.keyName, - ]); + return [ + Flame.images.load(Assets.images.bonusAnimation.dashNest.keyName), + Flame.images.load(Assets.images.bonusAnimation.sparkyTurboCharge.keyName), + Flame.images.load(Assets.images.bonusAnimation.dinoChomp.keyName), + Flame.images.load(Assets.images.bonusAnimation.androidSpaceship.keyName), + Flame.images.load(Assets.images.bonusAnimation.googleWord.keyName), + ]; + } + + @override + State createState() => _BonusAnimationState(); +} + +class _BonusAnimationState extends State + with TickerProviderStateMixin { + late SpriteAnimationController controller; + late SpriteAnimation animation; + bool shouldRunBuildCallback = true; + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + // When the animation is overwritten by another animation, we need to stop + // the callback in the build method as it will break the new animation. + // Otherwise we need to set up a new callback when a new animation starts to + // show the score view at the end of the animation. + @override + void didUpdateWidget(BonusAnimation oldWidget) { + shouldRunBuildCallback = oldWidget._imagePath == widget._imagePath; + + Future.delayed( + Duration(seconds: animation.totalDuration().ceil()), + () { + widget._onCompleted?.call(); + }, + ); + + super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { final spriteSheet = SpriteSheet.fromColumnsAndRows( - image: Flame.images.fromCache(imagePath), + image: Flame.images.fromCache(widget._imagePath), columns: 8, rows: 9, ); - final animation = spriteSheet.createAnimation( + animation = spriteSheet.createAnimation( row: 0, stepTime: 1 / 24, to: spriteSheet.rows * spriteSheet.columns, @@ -91,15 +134,22 @@ class BonusAnimation extends StatelessWidget { Future.delayed( Duration(seconds: animation.totalDuration().ceil()), () { - _onCompleted?.call(); + if (shouldRunBuildCallback) { + widget._onCompleted?.call(); + } }, ); + controller = SpriteAnimationController( + animation: animation, + vsync: this, + )..forward(); + return SizedBox( width: double.infinity, height: double.infinity, child: SpriteAnimationWidget( - animation: animation, + controller: controller, ), ); } diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 00eedd2b..3623e21f 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -1,46 +1,122 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/gen/gen.dart'; +import 'package:pinball/theme/app_colors.dart'; /// {@template game_hud} -/// Overlay of a [PinballGame] that displays the current [GameState.score] and -/// [GameState.balls]. +/// Overlay on the [PinballGame]. +/// +/// Displays the current [GameState.score], [GameState.balls] and animates when +/// the player gets a [GameBonus]. /// {@endtemplate} -class GameHud extends StatelessWidget { +class GameHud extends StatefulWidget { /// {@macro game_hud} const GameHud({Key? key}) : super(key: key); + @override + State createState() => _GameHudState(); +} + +class _GameHudState extends State { + bool showAnimation = false; + + /// Ratio from sprite frame (width 500, height 144) w / h = ratio + static const _ratio = 3.47; + static const _width = 265.0; + @override Widget build(BuildContext context) { - final state = context.watch().state; - - return Container( - color: Colors.redAccent, - width: 200, - height: 100, - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${state.score}', - style: Theme.of(context).textTheme.headline3, + final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + + return _ScoreViewDecoration( + child: SizedBox( + height: _width / _ratio, + width: _width, + child: BlocListener( + listenWhen: (previous, current) => + previous.bonusHistory.length != current.bonusHistory.length, + listener: (_, __) => setState(() => showAnimation = true), + child: AnimatedSwitcher( + duration: kThemeAnimationDuration, + child: showAnimation && !isGameOver + ? _AnimationView( + onComplete: () { + if (mounted) { + setState(() => showAnimation = false); + } + }, + ) + : const ScoreView(), ), - Wrap( - direction: Axis.vertical, - children: [ - for (var i = 0; i < state.balls; i++) - const Padding( - padding: EdgeInsets.only(top: 6, right: 6), - child: CircleAvatar( - radius: 8, - backgroundColor: Colors.black, - ), - ), - ], + ), + ), + ); + } +} + +class _ScoreViewDecoration extends StatelessWidget { + const _ScoreViewDecoration({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(12)); + const boardWidth = 5.0; + + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: radius, + border: Border.all( + color: AppColors.white, + width: boardWidth, + ), + image: DecorationImage( + fit: BoxFit.cover, + image: AssetImage( + Assets.images.score.miniScoreBackground.path, ), - ], + ), ), + child: Padding( + padding: const EdgeInsets.all(boardWidth - 1), + child: ClipRRect( + borderRadius: radius, + child: child, + ), + ), + ); + } +} + +class _AnimationView extends StatelessWidget { + const _AnimationView({ + Key? key, + required this.onComplete, + }) : super(key: key); + + final VoidCallback onComplete; + + @override + Widget build(BuildContext context) { + final lastBonus = context.select( + (GameBloc bloc) => bloc.state.bonusHistory.last, ); + switch (lastBonus) { + case GameBonus.dashNest: + return BonusAnimation.dashNest(onCompleted: onComplete); + case GameBonus.sparkyTurboCharge: + return BonusAnimation.sparkyTurboCharge(onCompleted: onComplete); + case GameBonus.dinoChomp: + return BonusAnimation.dinoChomp(onCompleted: onComplete); + case GameBonus.googleWord: + return BonusAnimation.googleWord(onCompleted: onComplete); + case GameBonus.androidSpaceship: + return BonusAnimation.androidSpaceship(onCompleted: onComplete); + } } } diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index ce5dce4b..f90ebb98 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:pinball/game/pinball_game.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; /// {@template play_button_overlay} /// [Widget] that renders the button responsible to starting the game diff --git a/lib/game/view/widgets/round_count_display.dart b/lib/game/view/widgets/round_count_display.dart new file mode 100644 index 00000000..98776764 --- /dev/null +++ b/lib/game/view/widgets/round_count_display.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; + +/// {@template round_count_display} +/// Colored square indicating if a round is available. +/// {@endtemplate} +class RoundCountDisplay extends StatelessWidget { + /// {@macro round_count_display} + const RoundCountDisplay({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + // TODO(arturplaczek): refactor when GameState handle balls and rounds and + // select state.rounds property instead of state.ball + final balls = context.select((GameBloc bloc) => bloc.state.balls); + + return Row( + children: [ + Text( + l10n.rounds, + style: AppTextStyle.subtitle1.copyWith( + color: AppColors.orange, + ), + ), + const SizedBox(width: 8), + Row( + children: [ + RoundIndicator(isActive: balls >= 1), + RoundIndicator(isActive: balls >= 2), + RoundIndicator(isActive: balls >= 3), + ], + ), + ], + ); + } +} + +/// {@template round_indicator} +/// [Widget] that displays the round indicator. +/// {@endtemplate} +@visibleForTesting +class RoundIndicator extends StatelessWidget { + /// {@macro round_indicator} + const RoundIndicator({ + Key? key, + required this.isActive, + }) : super(key: key); + + /// A value that describes whether the indicator is active. + final bool isActive; + + @override + Widget build(BuildContext context) { + final color = isActive ? AppColors.orange : AppColors.orange.withAlpha(128); + const size = 8.0; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Container( + color: color, + height: size, + width: size, + ), + ); + } +} diff --git a/lib/game/view/widgets/score_view.dart b/lib/game/view/widgets/score_view.dart new file mode 100644 index 00000000..288ea05c --- /dev/null +++ b/lib/game/view/widgets/score_view.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template score_view} +/// [Widget] that displays the score. +/// {@endtemplate} +class ScoreView extends StatelessWidget { + /// {@macro score_view} + const ScoreView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: AnimatedSwitcher( + duration: kThemeAnimationDuration, + child: isGameOver ? const _GameOver() : const _ScoreDisplay(), + ), + ); + } +} + +class _GameOver extends StatelessWidget { + const _GameOver({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Text( + l10n.gameOver, + style: AppTextStyle.headline1.copyWith( + color: AppColors.white, + ), + ); + } +} + +class _ScoreDisplay extends StatelessWidget { + const _ScoreDisplay({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + l10n.score.toLowerCase(), + style: AppTextStyle.subtitle1.copyWith( + color: AppColors.orange, + ), + ), + const _ScoreText(), + const RoundCountDisplay(), + ], + ); + } +} + +class _ScoreText extends StatelessWidget { + const _ScoreText({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final score = context.select((GameBloc bloc) => bloc.state.score); + + return Text( + score.formatScore(), + style: AppTextStyle.headline1.copyWith( + color: AppColors.white, + ), + ); + } +} diff --git a/lib/game/view/widgets/widgets.dart b/lib/game/view/widgets/widgets.dart index 674577af..5d1fccf8 100644 --- a/lib/game/view/widgets/widgets.dart +++ b/lib/game/view/widgets/widgets.dart @@ -1,3 +1,5 @@ export 'bonus_animation.dart'; export 'game_hud.dart'; export 'play_button_overlay.dart'; +export 'round_count_display.dart'; +export 'score_view.dart'; diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 3e52e399..f5b935a5 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -14,26 +14,27 @@ class $AssetsImagesGen { const $AssetsImagesBonusAnimationGen(); $AssetsImagesComponentsGen get components => const $AssetsImagesComponentsGen(); + $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); } class $AssetsImagesBonusAnimationGen { const $AssetsImagesBonusAnimationGen(); - /// File path: assets/images/bonus_animation/android.png - AssetGenImage get android => - const AssetGenImage('assets/images/bonus_animation/android.png'); + /// File path: assets/images/bonus_animation/android_spaceship.png + AssetGenImage get androidSpaceship => const AssetGenImage( + 'assets/images/bonus_animation/android_spaceship.png'); /// File path: assets/images/bonus_animation/dash_nest.png AssetGenImage get dashNest => const AssetGenImage('assets/images/bonus_animation/dash_nest.png'); - /// File path: assets/images/bonus_animation/dino.png - AssetGenImage get dino => - const AssetGenImage('assets/images/bonus_animation/dino.png'); + /// File path: assets/images/bonus_animation/dino_chomp.png + AssetGenImage get dinoChomp => + const AssetGenImage('assets/images/bonus_animation/dino_chomp.png'); - /// File path: assets/images/bonus_animation/google.png - AssetGenImage get google => - const AssetGenImage('assets/images/bonus_animation/google.png'); + /// File path: assets/images/bonus_animation/google_word.png + AssetGenImage get googleWord => + const AssetGenImage('assets/images/bonus_animation/google_word.png'); /// File path: assets/images/bonus_animation/sparky_turbo_charge.png AssetGenImage get sparkyTurboCharge => const AssetGenImage( @@ -48,6 +49,14 @@ class $AssetsImagesComponentsGen { const AssetGenImage('assets/images/components/background.png'); } +class $AssetsImagesScoreGen { + const $AssetsImagesScoreGen(); + + /// File path: assets/images/score/mini_score_background.png + AssetGenImage get miniScoreBackground => + const AssetGenImage('assets/images/score/mini_score_background.png'); +} + class Assets { Assets._(); diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c551535f..9655d8be 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -75,5 +75,9 @@ "enterInitials": "Enter your initials", "@enterInitials": { "description": "Text displayed on the ending dialog when game finishes to ask the user for his initials" + }, + "rounds": "Ball Ct:", + "@rounds": { + "description": "Text displayed on the scoreboard widget to indicate rounds left" } } diff --git a/lib/leaderboard/models/leader_board_entry.dart b/lib/leaderboard/models/leader_board_entry.dart index cfd5ffd4..a86975dd 100644 --- a/lib/leaderboard/models/leader_board_entry.dart +++ b/lib/leaderboard/models/leader_board_entry.dart @@ -36,7 +36,7 @@ extension LeaderboardEntryDataX on LeaderboardEntryData { rank: position.toString(), playerInitials: playerInitials, score: score, - character: character.toTheme.character, + character: character.toTheme.leaderboardIcon, ); } } diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart index 61e63d75..b9866111 100644 --- a/lib/leaderboard/view/leaderboard_page.dart +++ b/lib/leaderboard/view/leaderboard_page.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_theme/pinball_theme.dart'; class LeaderboardPage extends StatelessWidget { diff --git a/lib/theme/cubit/theme_cubit.dart b/lib/select_character/cubit/character_theme_cubit.dart similarity index 59% rename from lib/theme/cubit/theme_cubit.dart rename to lib/select_character/cubit/character_theme_cubit.dart index 94eba4a6..84792a71 100644 --- a/lib/theme/cubit/theme_cubit.dart +++ b/lib/select_character/cubit/character_theme_cubit.dart @@ -5,12 +5,12 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:pinball_theme/pinball_theme.dart'; -part 'theme_state.dart'; +part 'character_theme_state.dart'; -class ThemeCubit extends Cubit { - ThemeCubit() : super(const ThemeState.initial()); +class CharacterThemeCubit extends Cubit { + CharacterThemeCubit() : super(const CharacterThemeState.initial()); void characterSelected(CharacterTheme characterTheme) { - emit(ThemeState(PinballTheme(characterTheme: characterTheme))); + emit(CharacterThemeState(characterTheme)); } } diff --git a/lib/select_character/cubit/character_theme_state.dart b/lib/select_character/cubit/character_theme_state.dart new file mode 100644 index 00000000..ffe5667c --- /dev/null +++ b/lib/select_character/cubit/character_theme_state.dart @@ -0,0 +1,15 @@ +// ignore_for_file: public_member_api_docs +// TODO(allisonryan0002): Document this section when the API is stable. + +part of 'character_theme_cubit.dart'; + +class CharacterThemeState extends Equatable { + const CharacterThemeState(this.characterTheme); + + const CharacterThemeState.initial() : characterTheme = const DashTheme(); + + final CharacterTheme characterTheme; + + @override + List get props => [characterTheme]; +} diff --git a/lib/select_character/select_character.dart b/lib/select_character/select_character.dart new file mode 100644 index 00000000..40699840 --- /dev/null +++ b/lib/select_character/select_character.dart @@ -0,0 +1,2 @@ +export 'cubit/character_theme_cubit.dart'; +export 'view/view.dart'; diff --git a/lib/theme/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart similarity index 89% rename from lib/theme/view/character_selection_page.dart rename to lib/select_character/view/character_selection_page.dart index 22aaee22..0e83db8d 100644 --- a/lib/theme/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; -import 'package:pinball/theme/theme.dart'; import 'package:pinball_theme/pinball_theme.dart'; class CharacterSelectionDialog extends StatelessWidget { @@ -19,7 +19,7 @@ class CharacterSelectionDialog extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => ThemeCubit(), + create: (_) => CharacterThemeCubit(), child: const CharacterSelectionView(), ); } @@ -109,12 +109,14 @@ class CharacterImageButton extends StatelessWidget { @override Widget build(BuildContext context) { - final currentCharacterTheme = context.select( - (cubit) => cubit.state.theme.characterTheme, + final currentCharacterTheme = + context.select( + (cubit) => cubit.state.characterTheme, ); return GestureDetector( - onTap: () => context.read().characterSelected(characterTheme), + onTap: () => + context.read().characterSelected(characterTheme), child: DecoratedBox( decoration: BoxDecoration( color: (currentCharacterTheme == characterTheme) @@ -124,7 +126,7 @@ class CharacterImageButton extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.all(8), - child: characterTheme.character.image(), + child: characterTheme.icon.image(), ), ), ); diff --git a/lib/theme/view/view.dart b/lib/select_character/view/view.dart similarity index 100% rename from lib/theme/view/view.dart rename to lib/select_character/view/view.dart diff --git a/lib/theme/app_text_style.dart b/lib/theme/app_text_style.dart index 068f1eb9..8104ca11 100644 --- a/lib/theme/app_text_style.dart +++ b/lib/theme/app_text_style.dart @@ -5,7 +5,7 @@ import 'package:pinball/theme/theme.dart'; import 'package:pinball_components/pinball_components.dart'; const _fontPackage = 'pinball_components'; -const _primaryFontFamily = PinballFonts.pixeloidSans; +const _primaryFontFamily = FontFamily.pixeloidSans; abstract class AppTextStyle { static const headline1 = TextStyle( diff --git a/lib/theme/cubit/theme_state.dart b/lib/theme/cubit/theme_state.dart deleted file mode 100644 index 078f5c84..00000000 --- a/lib/theme/cubit/theme_state.dart +++ /dev/null @@ -1,16 +0,0 @@ -// ignore_for_file: public_member_api_docs -// TODO(allisonryan0002): Document this section when the API is stable. - -part of 'theme_cubit.dart'; - -class ThemeState extends Equatable { - const ThemeState(this.theme); - - const ThemeState.initial() - : theme = const PinballTheme(characterTheme: DashTheme()); - - final PinballTheme theme; - - @override - List get props => [theme]; -} diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 5e4fefe9..c9e2f9e1 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1,4 +1,2 @@ export 'app_colors.dart'; export 'app_text_style.dart'; -export 'cubit/theme_cubit.dart'; -export 'view/view.dart'; diff --git a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart index 30f6810f..9d8b2434 100644 --- a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart +++ b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart @@ -72,6 +72,20 @@ class FetchPlayerRankingException extends LeaderboardException { ); } +/// {@template fetch_prohibited_initials_exception} +/// Exception thrown when failure occurs while fetching prohibited initials. +/// {@endtemplate} +class FetchProhibitedInitialsException extends LeaderboardException { + /// {@macro fetch_prohibited_initials_exception} + const FetchProhibitedInitialsException( + Object error, + StackTrace stackTrace, + ) : super( + error, + stackTrace, + ); +} + /// {@template leaderboard_repository} /// Repository to access leaderboard data in Firebase Cloud Firestore. /// {@endtemplate} @@ -152,4 +166,25 @@ class LeaderboardRepository { throw FetchPlayerRankingException(error, stackTrace); } } + + /// Determines if the given [initials] are allowed. + Future areInitialsAllowed({required String initials}) async { + // Initials can only be three uppercase A-Z letters + final initialsRegex = RegExp(r'^[A-Z]{3}$'); + if (!initialsRegex.hasMatch(initials)) { + return false; + } + + try { + final document = await _firebaseFirestore + .collection('prohibitedInitials') + .doc('list') + .get(); + final prohibitedInitials = + document.get('prohibitedInitials') as List; + return !prohibitedInitials.contains(initials); + } on Exception catch (error, stackTrace) { + throw FetchProhibitedInitialsException(error, stackTrace); + } + } } diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart index 1341d3f4..9d31983f 100644 --- a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -21,6 +21,9 @@ class MockQueryDocumentSnapshot extends Mock class MockDocumentReference extends Mock implements DocumentReference> {} +class MockDocumentSnapshot extends Mock + implements DocumentSnapshot> {} + void main() { group('LeaderboardRepository', () { late FirebaseFirestore firestore; @@ -223,5 +226,94 @@ void main() { ); }); }); + + group('areInitialsAllowed', () { + late LeaderboardRepository leaderboardRepository; + late CollectionReference> collectionReference; + late DocumentReference> documentReference; + late DocumentSnapshot> documentSnapshot; + + setUp(() async { + collectionReference = MockCollectionReference(); + documentReference = MockDocumentReference(); + documentSnapshot = MockDocumentSnapshot(); + leaderboardRepository = LeaderboardRepository(firestore); + + when(() => firestore.collection('prohibitedInitials')) + .thenReturn(collectionReference); + when(() => collectionReference.doc('list')) + .thenReturn(documentReference); + when(() => documentReference.get()) + .thenAnswer((_) async => documentSnapshot); + when(() => documentSnapshot.get('prohibitedInitials')) + .thenReturn(['BAD']); + }); + + test('returns true if initials are three letters and allowed', () async { + final isUsernameAllowedResponse = + await leaderboardRepository.areInitialsAllowed( + initials: 'ABC', + ); + expect( + isUsernameAllowedResponse, + isTrue, + ); + }); + + test( + 'returns false if initials are shorter than 3 characters', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'AB'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test( + 'returns false if initials are longer than 3 characters', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'ABCD'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test( + 'returns false if initials contain a lowercase letter', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'AbC'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test( + 'returns false if initials contain a special character', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'A@C'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test('returns false if initials are forbidden', () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'BAD'); + expect(areInitialsAllowedResponse, isFalse); + }); + + test( + 'throws FetchProhibitedInitialsException when Exception occurs ' + 'when trying to retrieve information from firestore', + () async { + when(() => firestore.collection('prohibitedInitials')) + .thenThrow(Exception('oops')); + expect( + () => leaderboardRepository.areInitialsAllowed(initials: 'ABC'), + throwsA(isA()), + ); + }, + ); + }); }); } diff --git a/packages/pinball_components/assets/images/boundary/bottom.png b/packages/pinball_components/assets/images/boundary/bottom.png index 90bfa493..806f7051 100644 Binary files a/packages/pinball_components/assets/images/boundary/bottom.png and b/packages/pinball_components/assets/images/boundary/bottom.png differ diff --git a/packages/pinball_components/assets/images/boundary/outer-bottom.png b/packages/pinball_components/assets/images/boundary/outer-bottom.png new file mode 100644 index 00000000..508bcee8 Binary files /dev/null and b/packages/pinball_components/assets/images/boundary/outer-bottom.png differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-bottom.png b/packages/pinball_components/assets/images/dino/dino-land-bottom.png index 9aa42e12..6a20f1a7 100644 Binary files a/packages/pinball_components/assets/images/dino/dino-land-bottom.png and b/packages/pinball_components/assets/images/dino/dino-land-bottom.png differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-top.png b/packages/pinball_components/assets/images/dino/dino-land-top.png index 18b92541..cb4c82f2 100644 Binary files a/packages/pinball_components/assets/images/dino/dino-land-top.png and b/packages/pinball_components/assets/images/dino/dino-land-top.png differ diff --git a/packages/pinball_components/assets/images/sparky/animatronic.png b/packages/pinball_components/assets/images/sparky/animatronic.png new file mode 100644 index 00000000..cc57e405 Binary files /dev/null and b/packages/pinball_components/assets/images/sparky/animatronic.png differ diff --git a/packages/pinball_components/assets/images/sparky/computer/base.png b/packages/pinball_components/assets/images/sparky/computer/base.png index 2e8fe362..188e4329 100644 Binary files a/packages/pinball_components/assets/images/sparky/computer/base.png and b/packages/pinball_components/assets/images/sparky/computer/base.png differ diff --git a/packages/pinball_components/assets/images/sparky/computer/top.png b/packages/pinball_components/assets/images/sparky/computer/top.png index d9f3bc6c..085771cd 100644 Binary files a/packages/pinball_components/assets/images/sparky/computer/top.png and b/packages/pinball_components/assets/images/sparky/computer/top.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 0d67f870..4d4cda7d 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -86,6 +86,10 @@ class $AssetsImagesBoundaryGen { AssetGenImage get bottom => const AssetGenImage('assets/images/boundary/bottom.png'); + /// File path: assets/images/boundary/outer-bottom.png + AssetGenImage get outerBottom => + const AssetGenImage('assets/images/boundary/outer-bottom.png'); + /// File path: assets/images/boundary/outer.png AssetGenImage get outer => const AssetGenImage('assets/images/boundary/outer.png'); @@ -257,6 +261,8 @@ class $AssetsImagesSpaceshipGen { class $AssetsImagesSparkyGen { const $AssetsImagesSparkyGen(); + AssetGenImage get animatronic => + const AssetGenImage('assets/images/sparky/animatronic.png'); $AssetsImagesSparkyBumperGen get bumper => const $AssetsImagesSparkyBumperGen(); $AssetsImagesSparkyComputerGen get computer => diff --git a/packages/pinball_components/lib/gen/gen.dart b/packages/pinball_components/lib/gen/gen.dart index 0171b231..ada8b777 100644 --- a/packages/pinball_components/lib/gen/gen.dart +++ b/packages/pinball_components/lib/gen/gen.dart @@ -1,2 +1,3 @@ export 'assets.gen.dart'; +export 'fonts.gen.dart'; export 'pinball_fonts.dart'; diff --git a/packages/pinball_components/lib/src/components/boundaries.dart b/packages/pinball_components/lib/src/components/boundaries.dart index 254cbfcf..86e1844e 100644 --- a/packages/pinball_components/lib/src/components/boundaries.dart +++ b/packages/pinball_components/lib/src/components/boundaries.dart @@ -13,6 +13,7 @@ class Boundaries extends Blueprint { components: [ _BottomBoundary(), _OuterBoundary(), + _OuterBottomBoundarySpriteComponent(), ], ); } @@ -91,8 +92,10 @@ class _OuterBoundary extends BodyComponent with InitialPosition { /// {@macro outer_boundary} _OuterBoundary() : super( - priority: RenderPriority.outerBoudary, - children: [_OuterBoundarySpriteComponent()], + priority: RenderPriority.outerBoundary, + children: [ + _OuterBoundarySpriteComponent(), + ], ) { renderBody = false; } @@ -157,3 +160,25 @@ class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef { size = sprite.originalSize / 10; } } + +class _OuterBottomBoundarySpriteComponent extends SpriteComponent + with HasGameRef { + _OuterBottomBoundarySpriteComponent() + : super( + priority: RenderPriority.outerBottomBoundary, + anchor: Anchor.center, + position: Vector2(0, 71), + ); + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.boundary.outerBottom.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 9ea39d69..57e93abb 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -29,5 +29,6 @@ export 'slingshot.dart'; export 'spaceship.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; +export 'sparky_animatronic.dart'; export 'sparky_bumper.dart'; export 'sparky_computer.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_animatronic.dart b/packages/pinball_components/lib/src/components/dash_animatronic.dart index f1c5b145..47e1e08f 100644 --- a/packages/pinball_components/lib/src/components/dash_animatronic.dart +++ b/packages/pinball_components/lib/src/components/dash_animatronic.dart @@ -38,17 +38,9 @@ class DashAnimatronic extends SpriteAnimationComponent with HasGameRef { textureSize: textureSize, loop: false, ), - ); - } - - @override - void update(double dt) { - super.update(dt); - if (animation != null) { - if (animation!.isLastFrame) { - animation!.reset(); + )..onComplete = () { + animation?.reset(); playing = false; - } - } + }; } } diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index 9ce5a523..0654d038 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -34,24 +34,21 @@ class _DinoTopWall extends BodyComponent with InitialPosition { } List _createFixtureDefs() { - final fixturesDef = []; - final topStraightShape = EdgeShape() ..set( Vector2(28.65, -35.1), Vector2(29.5, -35.1), ); final topStraightFixtureDef = FixtureDef(topStraightShape); - fixturesDef.add(topStraightFixtureDef); final topCurveShape = BezierCurveShape( controlPoints: [ topStraightShape.vertex1, - Vector2(17.4, -26.38), - Vector2(25.5, -20.7), + Vector2(18.8, -27), + Vector2(26.6, -21), ], ); - fixturesDef.add(FixtureDef(topCurveShape)); + final topCurveFixtureDef = FixtureDef(topCurveShape); final middleCurveShape = BezierCurveShape( controlPoints: [ @@ -60,16 +57,16 @@ class _DinoTopWall extends BodyComponent with InitialPosition { Vector2(26.8, -19.5), ], ); - fixturesDef.add(FixtureDef(middleCurveShape)); + final middleCurveFixtureDef = FixtureDef(middleCurveShape); final bottomCurveShape = BezierCurveShape( controlPoints: [ middleCurveShape.vertices.last, - Vector2(21.5, -15.8), - Vector2(25.8, -14.8), + Vector2(23, -15), + Vector2(27, -15), ], ); - fixturesDef.add(FixtureDef(bottomCurveShape)); + final bottomCurveFixtureDef = FixtureDef(bottomCurveShape); final bottomStraightShape = EdgeShape() ..set( @@ -77,9 +74,14 @@ class _DinoTopWall extends BodyComponent with InitialPosition { Vector2(31, -14.5), ); final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); - fixturesDef.add(bottomStraightFixtureDef); - return fixturesDef; + return [ + topStraightFixtureDef, + topCurveFixtureDef, + middleCurveFixtureDef, + bottomCurveFixtureDef, + bottomStraightFixtureDef, + ]; } @override @@ -106,12 +108,14 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.dino.dinoLandTop.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.dinoLandTop.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(22, -41.8); + position = Vector2(22.8, -38.9); } } @@ -129,69 +133,56 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { } List _createFixtureDefs() { - final fixturesDef = []; const restitution = 1.0; - final topStraightControlPoints = [ - Vector2(32.4, -8.8), - Vector2(25, -7.7), - ]; final topStraightShape = EdgeShape() ..set( - topStraightControlPoints.first, - topStraightControlPoints.last, + Vector2(32.4, -8.8), + Vector2(25, -7.7), ); final topStraightFixtureDef = FixtureDef( topStraightShape, restitution: restitution, ); - fixturesDef.add(topStraightFixtureDef); - final topLeftCurveControlPoints = [ - topStraightControlPoints.last, - Vector2(21.8, -7), - Vector2(29.5, 13.8), - ]; final topLeftCurveShape = BezierCurveShape( - controlPoints: topLeftCurveControlPoints, + controlPoints: [ + topStraightShape.vertex2, + Vector2(21.8, -7), + Vector2(29.8, 13.8), + ], ); final topLeftCurveFixtureDef = FixtureDef( topLeftCurveShape, restitution: restitution, ); - fixturesDef.add(topLeftCurveFixtureDef); - final bottomLeftStraightControlPoints = [ - topLeftCurveControlPoints.last, - Vector2(31.8, 44.1), - ]; final bottomLeftStraightShape = EdgeShape() ..set( - bottomLeftStraightControlPoints.first, - bottomLeftStraightControlPoints.last, + topLeftCurveShape.vertices.last, + Vector2(31.9, 44.1), ); final bottomLeftStraightFixtureDef = FixtureDef( bottomLeftStraightShape, restitution: restitution, ); - fixturesDef.add(bottomLeftStraightFixtureDef); - final bottomStraightControlPoints = [ - bottomLeftStraightControlPoints.last, - Vector2(37.8, 44.1), - ]; final bottomStraightShape = EdgeShape() ..set( - bottomStraightControlPoints.first, - bottomStraightControlPoints.last, + bottomLeftStraightShape.vertex2, + Vector2(37.8, 44.1), ); final bottomStraightFixtureDef = FixtureDef( bottomStraightShape, restitution: restitution, ); - fixturesDef.add(bottomStraightFixtureDef); - return fixturesDef; + return [ + topStraightFixtureDef, + topLeftCurveFixtureDef, + bottomLeftStraightFixtureDef, + bottomStraightFixtureDef, + ]; } @override @@ -212,8 +203,10 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.dino.dinoLandBottom.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.dinoLandBottom.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; diff --git a/packages/pinball_components/lib/src/components/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter.dart index 1ed31435..43a4c113 100644 --- a/packages/pinball_components/lib/src/components/google_letter.dart +++ b/packages/pinball_components/lib/src/components/google_letter.dart @@ -78,6 +78,7 @@ class _GoogleLetterSprite extends SpriteComponent with HasGameRef { Future onLoad() async { await super.onLoad(); + // TODO(alestiago): Used cached assets. final sprite = await gameRef.loadSprite(_path); this.sprite = sprite; // TODO(alestiago): Size correctly once the assets are provided. diff --git a/packages/pinball_components/lib/src/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker.dart index 26e6c8f9..f6963d7c 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker.dart @@ -136,6 +136,7 @@ class _KickerSpriteComponent extends SpriteComponent with HasGameRef { Future onLoad() async { await super.onLoad(); + // TODO(alestiago): Used cached asset. final sprite = await gameRef.loadSprite( (_side.isLeft) ? Assets.images.kicker.left.keyName diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart index efa8817a..295c799d 100644 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -148,6 +148,7 @@ class _PlungerSpriteAnimationGroupComponent Future onLoad() async { await super.onLoad(); + // TODO(alestiago): Used cached images. final spriteSheet = await gameRef.images.load( Assets.images.plunger.plunger.keyName, ); diff --git a/packages/pinball_components/lib/src/components/render_priority.dart b/packages/pinball_components/lib/src/components/render_priority.dart index b179cce9..d2438db9 100644 --- a/packages/pinball_components/lib/src/components/render_priority.dart +++ b/packages/pinball_components/lib/src/components/render_priority.dart @@ -39,7 +39,9 @@ abstract class RenderPriority { static const int bottomBoundary = _above + dinoBottomWall; - static const int outerBoudary = _above + background; + static const int outerBoundary = _above + background; + + static const int outerBottomBoundary = _above + rocket; // Bottom Group @@ -47,7 +49,7 @@ abstract class RenderPriority { // Launcher - static const int launchRamp = _above + outerBoudary; + static const int launchRamp = _above + outerBoundary; static const int launchRampForegroundRailing = _below + ballOnBoard; @@ -113,4 +115,7 @@ abstract class RenderPriority { // Score Text static const int scoreText = _above + spaceshipRampForegroundRailing; + + // Debug information + static const int debugInfo = _above + scoreText; } diff --git a/packages/pinball_components/lib/src/components/sparky_animatronic.dart b/packages/pinball_components/lib/src/components/sparky_animatronic.dart new file mode 100644 index 00000000..714a5700 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_animatronic.dart @@ -0,0 +1,46 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template sparky_animatronic} +/// Animated Sparky that sits on top of the [SparkyComputer]. +/// {@endtemplate} +class SparkyAnimatronic extends SpriteAnimationComponent with HasGameRef { + /// {@macro sparky_animatronic} + SparkyAnimatronic() + : super( + anchor: Anchor.center, + playing: false, + priority: RenderPriority.sparkyAnimatronic, + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.sparky.animatronic.keyName, + ); + + const amountPerRow = 9; + const amountPerColumn = 7; + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: (amountPerRow * amountPerColumn) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + loop: false, + ), + )..onComplete = () { + animation?.reset(); + playing = false; + }; + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_computer.dart b/packages/pinball_components/lib/src/components/sparky_computer.dart index 4cec4c59..481de63d 100644 --- a/packages/pinball_components/lib/src/components/sparky_computer.dart +++ b/packages/pinball_components/lib/src/components/sparky_computer.dart @@ -7,9 +7,6 @@ import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_computer} /// A computer owned by Sparky. -/// -/// Register a [ContactCallback] for [SparkyComputerSensor] to listen when -/// something enters the [SparkyComputer]. /// {@endtemplate} class SparkyComputer extends Blueprint { /// {@macro sparky_computer} @@ -18,7 +15,6 @@ class SparkyComputer extends Blueprint { components: [ _ComputerBase(), _ComputerTopSpriteComponent(), - SparkyComputerSensor(), ], ); } @@ -104,24 +100,3 @@ class _ComputerTopSpriteComponent extends SpriteComponent with HasGameRef { size = sprite.originalSize / 10; } } - -/// {@template sparky_computer_sensor} -/// Small sensor body used to detect when a ball has entered the -/// [SparkyComputer]. -/// {@endtemplate} -class SparkyComputerSensor extends BodyComponent with InitialPosition { - /// {@macro sparky_computer_sensor} - SparkyComputerSensor() { - renderBody = false; - } - - @override - Body createBody() { - final shape = CircleShape()..radius = 0.1; - final fixtureDef = FixtureDef(shape, isSensor: true); - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 7df3429a..a7cb8367 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -61,6 +61,7 @@ flutter: - assets/images/slingshot/ - assets/images/alien_bumper/a/ - assets/images/alien_bumper/b/ + - assets/images/sparky/ - assets/images/sparky/computer/ - assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/b/ diff --git a/packages/pinball_components/sandbox/lib/common/add_game.dart b/packages/pinball_components/sandbox/lib/common/add_game.dart new file mode 100644 index 00000000..5b6388d3 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/common/add_game.dart @@ -0,0 +1,36 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; + +const _path = + 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/'; + +extension StoryAddGame on Story { + void addGame({ + required String title, + required String description, + required Game Function(DashbookContext) gameBuilder, + }) { + final _chapter = Chapter( + title, + (DashbookContext context) { + final game = gameBuilder(context); + if (game is Traceable) { + game.trace = context.boolProperty('Trace', true); + } + + return GameWidget(game: game); + }, + this, + codeLink: '$_path${name.toPath()}/${title.toPath()}', + info: description, + ); + chapters.add(_chapter); + } +} + +extension on String { + String toPath() { + return replaceAll(' ', '_')..toLowerCase(); + } +} diff --git a/packages/pinball_components/sandbox/lib/common/common.dart b/packages/pinball_components/sandbox/lib/common/common.dart index bb232e24..53010f5e 100644 --- a/packages/pinball_components/sandbox/lib/common/common.dart +++ b/packages/pinball_components/sandbox/lib/common/common.dart @@ -1,3 +1,3 @@ +export 'add_game.dart'; export 'games.dart'; -export 'methods.dart'; export 'trace.dart'; diff --git a/packages/pinball_components/sandbox/lib/common/games.dart b/packages/pinball_components/sandbox/lib/common/games.dart index 4fb158bd..89d16450 100644 --- a/packages/pinball_components/sandbox/lib/common/games.dart +++ b/packages/pinball_components/sandbox/lib/common/games.dart @@ -5,16 +5,25 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; -abstract class BasicGame extends Forge2DGame { - BasicGame() { +abstract class AssetsGame extends Forge2DGame { + AssetsGame({ + List? imagesFileNames, + }) : _imagesFileNames = imagesFileNames { images.prefix = ''; } -} -abstract class BasicKeyboardGame extends BasicGame - with HasKeyboardHandlerComponents {} + final List? _imagesFileNames; + + @override + Future onLoad() async { + await super.onLoad(); + if (_imagesFileNames != null) { + await images.loadAll(_imagesFileNames!); + } + } +} -abstract class LineGame extends BasicGame with PanDetector { +abstract class LineGame extends AssetsGame with PanDetector { Vector2? _lineEnd; @override diff --git a/packages/pinball_components/sandbox/lib/common/methods.dart b/packages/pinball_components/sandbox/lib/common/methods.dart deleted file mode 100644 index 35198922..00000000 --- a/packages/pinball_components/sandbox/lib/common/methods.dart +++ /dev/null @@ -1,3 +0,0 @@ -String buildSourceLink(String path) { - return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path'; -} diff --git a/packages/pinball_components/sandbox/lib/common/trace.dart b/packages/pinball_components/sandbox/lib/common/trace.dart index d35568ea..6129a779 100644 --- a/packages/pinball_components/sandbox/lib/common/trace.dart +++ b/packages/pinball_components/sandbox/lib/common/trace.dart @@ -35,7 +35,7 @@ mixin Traceable on Forge2DGame { .forEach((bodyComponent) => bodyComponent.trace()); descendants() - .whereType() + .whereType() .forEach((sprite) => sprite.setOpacity(0.5)); } } diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 73ae296c..e5f7f177 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -16,9 +16,6 @@ void main() { addLayerStories(dashbook); addEffectsStories(dashbook); addFlipperStories(dashbook); - addSpaceshipStories(dashbook); - addSpaceshipRampStories(dashbook); - addSpaceshipRailStories(dashbook); addBaseboardStories(dashbook); addChromeDinoStories(dashbook); addDashNestBumperStories(dashbook); @@ -27,12 +24,12 @@ void main() { addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); addAlienZoneStories(dashbook); - addZoomStories(dashbook); addBoundariesStories(dashbook); addGoogleWordStories(dashbook); addLaunchRampStories(dashbook); addScoreTextStories(dashbook); addBackboardStories(dashbook); + addDinoWallStories(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 index fec10fe0..4832a468 100644 --- 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 @@ -4,10 +4,17 @@ 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 = ''' +class AlienBumperAGame extends BallGame { + AlienBumperAGame() + : super( + color: const Color(0xFF0000FF), + imagesFileNames: [ + Assets.images.alienBumper.a.active.keyName, + Assets.images.alienBumper.a.inactive.keyName, + ], + ); + + static const description = ''' Shows how a AlienBumperA is rendered. - Activate the "trace" parameter to overlay the body. @@ -17,16 +24,10 @@ class AlienBumperAGame extends BasicBallGame { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.alienBumper.a.active.keyName, - Assets.images.alienBumper.a.inactive.keyName, - ]); - - final center = screenToWorld(camera.viewport.canvasSize! / 2); - final alienBumperA = AlienBumper.a() - ..initialPosition = Vector2(center.x - 20, center.y - 20) - ..priority = 1; - await add(alienBumperA); + camera.followVector2(Vector2.zero()); + await add( + AlienBumper.a()..priority = 1, + ); 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 index 0218127b..abb206ca 100644 --- 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 @@ -4,10 +4,17 @@ 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 = ''' +class AlienBumperBGame extends BallGame { + AlienBumperBGame() + : super( + color: const Color(0xFF0000FF), + imagesFileNames: [ + Assets.images.alienBumper.b.active.keyName, + Assets.images.alienBumper.b.inactive.keyName, + ], + ); + + static const description = ''' Shows how a AlienBumperB is rendered. - Activate the "trace" parameter to overlay the body. @@ -17,16 +24,10 @@ class AlienBumperBGame extends BasicBallGame { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.alienBumper.b.active.keyName, - Assets.images.alienBumper.b.inactive.keyName, - ]); - - final center = screenToWorld(camera.viewport.canvasSize! / 2); - final alienBumperB = AlienBumper.b() - ..initialPosition = Vector2(center.x - 10, center.y + 10) - ..priority = 1; - await add(alienBumperB); + camera.followVector2(Vector2.zero()); + await add( + AlienBumper.b()..priority = 1, + ); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_game.dart similarity index 79% rename from packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart rename to packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_game.dart index 6c00f476..ad897dd4 100644 --- a/packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_game.dart @@ -6,8 +6,8 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/common/common.dart'; -class BasicSpaceshipGame extends BasicGame with TapDetector { - static const info = ''' +class SpaceshipGame extends AssetsGame with TapDetector { + static const description = ''' Shows how a Spaceship works. - Tap anywhere on the screen to spawn a Ball into the game. @@ -18,10 +18,10 @@ class BasicSpaceshipGame extends BasicGame with TapDetector { await super.onLoad(); camera.followVector2(Vector2.zero()); - - unawaited( - addFromBlueprint(Spaceship(position: Vector2.zero())), + await addFromBlueprint( + Spaceship(position: Vector2.zero()), ); + await ready(); } @override diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_rail/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_rail_game.dart similarity index 82% rename from packages/pinball_components/sandbox/lib/stories/spaceship_rail/spaceship_rail_game.dart rename to packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_rail_game.dart index 471333c3..2a13fb5e 100644 --- a/packages/pinball_components/sandbox/lib/stories/spaceship_rail/spaceship_rail_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_rail_game.dart @@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SpaceshipRailGame extends BasicBallGame { +class SpaceshipRailGame extends BallGame { SpaceshipRailGame() : super( color: Colors.blue, @@ -14,7 +14,7 @@ class SpaceshipRailGame extends BasicBallGame { ballLayer: Layer.spaceshipExitRail, ); - static const info = ''' + static const description = ''' Shows how SpaceshipRail are rendered. - Activate the "trace" parameter to overlay the body. @@ -26,10 +26,8 @@ class SpaceshipRailGame extends BasicBallGame { await super.onLoad(); camera.followVector2(Vector2(-30, -10)); - - final spaceshipRail = SpaceshipRail(); - unawaited(addFromBlueprint(spaceshipRail)); - + await addFromBlueprint(SpaceshipRail()); + await ready(); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_ramp_game.dart similarity index 59% rename from packages/pinball_components/sandbox/lib/stories/spaceship_ramp/spaceship_ramp_game.dart rename to packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_ramp_game.dart index f259b28b..1817f40a 100644 --- a/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_ramp_game.dart @@ -7,15 +7,27 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SpaceshipRampGame extends BasicBallGame with KeyboardEvents { +class SpaceshipRampGame extends BallGame with KeyboardEvents { SpaceshipRampGame() : super( color: Colors.blue, ballPriority: RenderPriority.ballOnSpaceshipRamp, ballLayer: Layer.spaceshipEntranceRamp, + imagesFileNames: [ + Assets.images.spaceship.ramp.railingBackground.keyName, + Assets.images.spaceship.ramp.main.keyName, + Assets.images.spaceship.ramp.boardOpening.keyName, + Assets.images.spaceship.ramp.railingForeground.keyName, + Assets.images.spaceship.ramp.arrow.inactive.keyName, + Assets.images.spaceship.ramp.arrow.active1.keyName, + Assets.images.spaceship.ramp.arrow.active2.keyName, + Assets.images.spaceship.ramp.arrow.active3.keyName, + Assets.images.spaceship.ramp.arrow.active4.keyName, + Assets.images.spaceship.ramp.arrow.active5.keyName, + ], ); - static const info = ''' + static const description = ''' Shows how SpaceshipRamp is rendered. - Activate the "trace" parameter to overlay the body. @@ -29,22 +41,10 @@ class SpaceshipRampGame extends BasicBallGame with KeyboardEvents { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.spaceship.ramp.railingBackground.keyName, - Assets.images.spaceship.ramp.main.keyName, - Assets.images.spaceship.ramp.boardOpening.keyName, - Assets.images.spaceship.ramp.railingForeground.keyName, - Assets.images.spaceship.ramp.arrow.inactive.keyName, - Assets.images.spaceship.ramp.arrow.active1.keyName, - Assets.images.spaceship.ramp.arrow.active2.keyName, - Assets.images.spaceship.ramp.arrow.active3.keyName, - Assets.images.spaceship.ramp.arrow.active4.keyName, - Assets.images.spaceship.ramp.arrow.active5.keyName, - ]); - - _spaceshipRamp = SpaceshipRamp(); - await addFromBlueprint(_spaceshipRamp); camera.followVector2(Vector2(-12, -50)); + await addFromBlueprint( + _spaceshipRamp = SpaceshipRamp(), + ); 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 index 4bc758f9..b4e7c1b6 100644 --- a/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart @@ -1,25 +1,36 @@ 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'; +import 'package:sandbox/stories/alien_zone/spaceship_game.dart'; +import 'package:sandbox/stories/alien_zone/spaceship_rail_game.dart'; +import 'package:sandbox/stories/alien_zone/spaceship_ramp_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, + ..addGame( + title: 'Alien Bumper A', + description: AlienBumperAGame.description, + gameBuilder: (_) => AlienBumperAGame(), ) - ..add( - 'Alien Bumper B', - (context) => GameWidget( - game: AlienBumperBGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('alien_zone/alien_bumper_b.dart'), - info: AlienBumperAGame.info, + ..addGame( + title: 'Alien Bumper B', + description: AlienBumperBGame.description, + gameBuilder: (_) => AlienBumperBGame(), + ) + ..addGame( + title: 'Spaceship', + description: SpaceshipGame.description, + gameBuilder: (_) => SpaceshipGame(), + ) + ..addGame( + title: 'Spaceship Rail', + description: SpaceshipRailGame.description, + gameBuilder: (_) => SpaceshipRailGame(), + ) + ..addGame( + title: 'Spaceship Ramp', + description: SpaceshipRampGame.description, + gameBuilder: (_) => SpaceshipRampGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart index 691b1656..639a4b57 100644 --- a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart @@ -1,21 +1,22 @@ -import 'package:flame/components.dart'; +import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart'; import 'package:sandbox/common/common.dart'; -class BackboardGameOverGame extends BasicKeyboardGame { - BackboardGameOverGame(this.score, this.character); +class BackboardGameOverGame extends AssetsGame + with HasKeyboardHandlerComponents { + BackboardGameOverGame(this.score, this.character) + : super( + imagesFileNames: characterIconPaths.values.toList(), + ); - static const info = ''' - Simple example showing the game over mode of the backboard. + static const description = ''' + Shows how the Backboard in game over mode is rendered. - Select a character to update the character icon. '''; - final int score; - final String character; - static final characterIconPaths = { 'Dash': Assets.images.dash.leaderboardIcon.keyName, 'Sparky': Assets.images.sparky.leaderboardIcon.keyName, @@ -23,14 +24,16 @@ class BackboardGameOverGame extends BasicKeyboardGame { 'Dino': Assets.images.dino.leaderboardIcon.keyName, }; + final int score; + + final String character; + @override Future onLoad() async { camera ..followVector2(Vector2.zero()) ..zoom = 5; - await images.loadAll(characterIconPaths.values.toList()); - await add( Backboard.gameOver( position: Vector2(0, 20), diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart index 71f5c09a..6da9206c 100644 --- a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart @@ -2,9 +2,14 @@ import 'package:flame/components.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; -class BackboardWaitingGame extends BasicGame { - static const info = ''' - Simple example showing the waiting mode of the backboard. +class BackboardWaitingGame extends AssetsGame { + BackboardWaitingGame() + : super( + imagesFileNames: [], + ); + + static const description = ''' + Shows how the Backboard in waiting mode is rendered. '''; @override @@ -13,7 +18,8 @@ class BackboardWaitingGame extends BasicGame { ..followVector2(Vector2.zero()) ..zoom = 5; - final backboard = Backboard.waiting(position: Vector2(0, 20)); - await add(backboard); + await add( + Backboard.waiting(position: Vector2(0, 20)), + ); } } diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart index cc05ba4f..b8c85d10 100644 --- a/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart @@ -1,32 +1,25 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/backboard/backboard_game_over_game.dart'; import 'package:sandbox/stories/backboard/backboard_waiting_game.dart'; void addBackboardStories(Dashbook dashbook) { dashbook.storiesOf('Backboard') - ..add( - 'Waiting mode', - (context) => GameWidget( - game: BackboardWaitingGame(), - ), - codeLink: buildSourceLink('backboard/waiting.dart'), - info: BackboardWaitingGame.info, + ..addGame( + title: 'Waiting', + description: BackboardWaitingGame.description, + gameBuilder: (_) => BackboardWaitingGame(), ) - ..add( - 'Game over', - (context) => GameWidget( - game: BackboardGameOverGame( - context.numberProperty('Score', 9000000000).toInt(), - context.listProperty( - 'Character', - BackboardGameOverGame.characterIconPaths.keys.first, - BackboardGameOverGame.characterIconPaths.keys.toList(), - ), + ..addGame( + title: 'Game over', + description: BackboardGameOverGame.description, + gameBuilder: (context) => BackboardGameOverGame( + context.numberProperty('Score', 9000000000).toInt(), + context.listProperty( + 'Character', + BackboardGameOverGame.characterIconPaths.keys.first, + BackboardGameOverGame.characterIconPaths.keys.toList(), ), ), - codeLink: buildSourceLink('backboard/game_over.dart'), - info: BackboardGameOverGame.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 2fa4462e..7f07de97 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 @@ -4,7 +4,7 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; class BallBoosterGame extends LineGame { - static const info = ''' + static const description = ''' Shows how a Ball with a boost works. - Drag to launch a boosted Ball. 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 17bd0723..e57a0322 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 @@ -3,14 +3,20 @@ import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; -class BasicBallGame extends BasicGame with TapDetector, Traceable { - BasicBallGame({ +class BallGame extends AssetsGame with TapDetector, Traceable { + BallGame({ this.color = Colors.blue, this.ballPriority = 0, this.ballLayer = Layer.all, - }); + List? imagesFileNames, + }) : super( + imagesFileNames: [ + Assets.images.ball.ball.keyName, + if (imagesFileNames != null) ...imagesFileNames, + ], + ); - static const info = ''' + static const description = ''' Shows how a Ball works. - Tap anywhere on the screen to spawn a ball into the game. diff --git a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart index 64892d22..eb472282 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart @@ -1,5 +1,4 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/ball_booster_game.dart'; @@ -7,22 +6,16 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; void addBallStories(Dashbook dashbook) { dashbook.storiesOf('Ball') - ..add( - 'Basic', - (context) => GameWidget( - game: BasicBallGame( - color: context.colorProperty('color', Colors.blue), - )..trace = context.boolProperty('Trace', true), + ..addGame( + title: 'Colored', + description: BallGame.description, + gameBuilder: (context) => BallGame( + color: context.colorProperty('color', Colors.blue), ), - codeLink: buildSourceLink('ball/basic.dart'), - info: BasicBallGame.info, ) - ..add( - 'Booster', - (context) => GameWidget( - game: BallBoosterGame(), - ), - codeLink: buildSourceLink('ball/ball_booster.dart'), - info: BallBoosterGame.info, + ..addGame( + title: 'Booster', + description: BallBoosterGame.description, + gameBuilder: (context) => BallBoosterGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart index 3dab5c6f..4e86f732 100644 --- a/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart @@ -1,10 +1,17 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class BaseboardGame extends BasicBallGame with Traceable { - static const info = ''' +class BaseboardGame extends BallGame { + BaseboardGame() + : super( + imagesFileNames: [ + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + ], + ); + + static const description = ''' Shows how the Baseboards are rendered. - Activate the "trace" parameter to overlay the body. @@ -15,22 +22,14 @@ class BaseboardGame extends BasicBallGame with Traceable { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.baseboard.left.keyName, - Assets.images.baseboard.right.keyName, - ]); - final center = screenToWorld(camera.viewport.canvasSize! / 2); - final leftBaseboard = Baseboard(side: BoardSide.left) - ..initialPosition = center - Vector2(25, 0) - ..priority = 1; - final rightBaseboard = Baseboard(side: BoardSide.right) - ..initialPosition = center + Vector2(25, 0) - ..priority = 1; - await addAll([ - leftBaseboard, - rightBaseboard, + Baseboard(side: BoardSide.left) + ..initialPosition = center - Vector2(25, 0) + ..priority = 1, + Baseboard(side: BoardSide.right) + ..initialPosition = center + Vector2(25, 0) + ..priority = 1, ]); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart index 2e4d3090..b07e3a73 100644 --- a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/baseboard/baseboard_game.dart'; void addBaseboardStories(Dashbook dashbook) { - dashbook.storiesOf('Baseboard').add( - 'Basic', - (context) => GameWidget( - game: BaseboardGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('baseboard_game/basic.dart'), - info: BaseboardGame.info, + dashbook.storiesOf('Baseboard').addGame( + title: 'Traced', + description: BaseboardGame.description, + gameBuilder: (_) => BaseboardGame(), ); } 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 ca7c8cc8..cf78750d 100644 --- a/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart @@ -1,11 +1,19 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class BoundariesGame extends BasicBallGame with Traceable { - static const info = ''' +class BoundariesGame extends BallGame { + BoundariesGame() + : super( + imagesFileNames: [ + Assets.images.boundary.outer.keyName, + Assets.images.boundary.outerBottom.keyName, + Assets.images.boundary.bottom.keyName, + ], + ); + + static const description = ''' Shows how Boundaries are rendered. - Activate the "trace" parameter to overlay the body. @@ -16,18 +24,11 @@ class BoundariesGame extends BasicBallGame with Traceable { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.boundary.outer.keyName, - Assets.images.boundary.bottom.keyName, - ]); - - await addFromBlueprint(Boundaries()); - await ready(); - camera ..followVector2(Vector2.zero()) ..zoom = 6; - + await addFromBlueprint(Boundaries()); + await ready(); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/boundaries/stories.dart b/packages/pinball_components/sandbox/lib/stories/boundaries/stories.dart index bebb0df7..02bb87b4 100644 --- a/packages/pinball_components/sandbox/lib/stories/boundaries/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/boundaries/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/boundaries/boundaries_game.dart'; void addBoundariesStories(Dashbook dashbook) { - dashbook.storiesOf('Boundaries').add( - 'Basic', - (context) => GameWidget( - game: BoundariesGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('boundaries_game/basic.dart'), - info: BoundariesGame.info, + dashbook.storiesOf('Boundaries').addGame( + title: 'Traced', + description: BoundariesGame.description, + gameBuilder: (_) => BoundariesGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart index 94bf6e44..2e6831e3 100644 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart @@ -2,7 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; class ChromeDinoGame extends Forge2DGame { - static const info = 'Shows how a ChromeDino is rendered.'; + static const description = 'Shows how a ChromeDino is rendered.'; @override Future onLoad() async { diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart index fb7c2ee1..391cdca7 100644 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/chrome_dino/chrome_dino_game.dart'; void addChromeDinoStories(Dashbook dashbook) { - dashbook.storiesOf('Chrome Dino').add( - 'Basic', - (context) => GameWidget( - game: ChromeDinoGame(), - ), - codeLink: buildSourceLink('chrome_dino/basic.dart'), - info: ChromeDinoGame.info, + dashbook.storiesOf('Chrome Dino').addGame( + title: 'Trace', + description: ChromeDinoGame.description, + gameBuilder: (_) => ChromeDinoGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart new file mode 100644 index 00000000..b491d64b --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class DinoWallGame extends BallGame { + DinoWallGame() : super(); + + static const description = ''' + Shows how DinoWalls 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(); + + await images.loadAll([ + Assets.images.dino.dinoLandTop.keyName, + Assets.images.dino.dinoLandBottom.keyName, + ]); + + await addFromBlueprint(DinoWalls()); + camera.followVector2(Vector2.zero()); + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart new file mode 100644 index 00000000..e24d26cc --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/dino_wall/dino_wall_game.dart'; + +void addDinoWallStories(Dashbook dashbook) { + dashbook.storiesOf('DinoWall').addGame( + title: 'Traced', + description: DinoWallGame.description, + gameBuilder: (_) => DinoWallGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart b/packages/pinball_components/sandbox/lib/stories/effects/camera_zoom_game.dart similarity index 89% rename from packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart rename to packages/pinball_components/sandbox/lib/stories/effects/camera_zoom_game.dart index f1c17fe9..11f2b776 100644 --- a/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/effects/camera_zoom_game.dart @@ -3,8 +3,8 @@ import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; -class BasicCameraZoomGame extends BasicGame with TapDetector { - static const info = ''' +class CameraZoomGame extends AssetsGame with TapDetector { + static const description = ''' Shows how CameraZoom can be used. - Tap to zoom in/out. 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 9e2acaf4..3ca8ec1a 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 @@ -3,7 +3,7 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; class FireEffectGame extends LineGame { - static const info = ''' + static const description = ''' Shows how the FireEffect renders. - Drag a line to define the trail direction. diff --git a/packages/pinball_components/sandbox/lib/stories/effects/stories.dart b/packages/pinball_components/sandbox/lib/stories/effects/stories.dart index 37ba434e..9b022987 100644 --- a/packages/pinball_components/sandbox/lib/stories/effects/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/effects/stories.dart @@ -1,13 +1,18 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/effects/camera_zoom_game.dart'; import 'package:sandbox/stories/effects/fire_effect_game.dart'; void addEffectsStories(Dashbook dashbook) { - dashbook.storiesOf('Effects').add( - 'Fire Effect', - (context) => GameWidget(game: FireEffectGame()), - codeLink: buildSourceLink('effects/fire_effect.dart'), - info: FireEffectGame.info, - ); + dashbook.storiesOf('Effects') + ..addGame( + title: 'Fire', + description: FireEffectGame.description, + gameBuilder: (_) => FireEffectGame(), + ) + ..addGame( + title: 'CameraZoom', + description: CameraZoomGame.description, + gameBuilder: (_) => CameraZoomGame(), + ); } 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 40a638b9..789fa8b4 100644 --- a/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart @@ -2,12 +2,19 @@ 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 FlipperGame extends BasicBallGame with KeyboardEvents, Traceable { - static const info = ''' +class FlipperGame extends BallGame with KeyboardEvents { + FlipperGame() + : super( + imagesFileNames: [ + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ], + ); + + static const description = ''' Shows how Flippers are rendered. - Activate the "trace" parameter to overlay the body. @@ -33,21 +40,12 @@ class FlipperGame extends BasicBallGame with KeyboardEvents, Traceable { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - ]); - final center = screenToWorld(camera.viewport.canvasSize! / 2); - - leftFlipper = Flipper(side: BoardSide.left) - ..initialPosition = center - Vector2(Flipper.size.x, 0); - rightFlipper = Flipper(side: BoardSide.right) - ..initialPosition = center + Vector2(Flipper.size.x, 0); - await addAll([ - leftFlipper, - rightFlipper, + leftFlipper = Flipper(side: BoardSide.left) + ..initialPosition = center - Vector2(Flipper.size.x, 0), + rightFlipper = Flipper(side: BoardSide.right) + ..initialPosition = center + Vector2(Flipper.size.x, 0), ]); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart b/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart index f8aa0075..2ef2a4b6 100644 --- a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/flipper/flipper_game.dart'; void addFlipperStories(Dashbook dashbook) { - dashbook.storiesOf('Flipper').add( - 'Basic', - (context) => GameWidget( - game: FlipperGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flipper/basic.dart'), - info: FlipperGame.info, + dashbook.storiesOf('Flipper').addGame( + title: 'Traced', + description: FlipperGame.description, + gameBuilder: (_) => FlipperGame(), ); } 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 1ccd7e8e..3580a175 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 @@ -2,11 +2,18 @@ import 'dart:async'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class BigDashNestBumperGame extends BasicBallGame with Traceable { - static const info = ''' +class BigDashNestBumperGame extends BallGame { + BigDashNestBumperGame() + : super( + imagesFileNames: [ + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + ], + ); + + static const description = ''' Shows how a BigDashNestBumper is rendered. - Activate the "trace" parameter to overlay the body. @@ -16,13 +23,10 @@ class BigDashNestBumperGame extends BasicBallGame with Traceable { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, - ]); - camera.followVector2(Vector2.zero()); - await add(DashNestBumper.main()..priority = 1); + await add( + DashNestBumper.main()..priority = 1, + ); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart index fd7ce93c..349dd811 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart @@ -2,11 +2,20 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SignpostGame extends BasicBallGame with Traceable, TapDetector { - static const info = ''' +class SignpostGame extends BallGame { + SignpostGame() + : super( + imagesFileNames: [ + Assets.images.signpost.inactive.keyName, + Assets.images.signpost.active1.keyName, + Assets.images.signpost.active2.keyName, + Assets.images.signpost.active3.keyName, + ], + ); + + static const description = ''' Shows how a Signpost is rendered. - Activate the "trace" parameter to overlay the body. @@ -17,13 +26,6 @@ class SignpostGame extends BasicBallGame with Traceable, TapDetector { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.signpost.inactive.keyName, - Assets.images.signpost.active1.keyName, - Assets.images.signpost.active2.keyName, - Assets.images.signpost.active3.keyName, - ]); - camera.followVector2(Vector2.zero()); await add(Signpost()); await traceAllBodies(); 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 b0a621fa..071f6aa1 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 @@ -2,11 +2,18 @@ import 'dart:async'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SmallDashNestBumperAGame extends BasicBallGame with Traceable { - static const info = ''' +class SmallDashNestBumperAGame extends BallGame { + SmallDashNestBumperAGame() + : super( + imagesFileNames: [ + Assets.images.dash.bumper.a.active.keyName, + Assets.images.dash.bumper.a.inactive.keyName, + ], + ); + + static const description = ''' Shows how a SmallDashNestBumper ("a") is rendered. - Activate the "trace" parameter to overlay the body. @@ -16,11 +23,6 @@ class SmallDashNestBumperAGame extends BasicBallGame with Traceable { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.dash.bumper.a.active.keyName, - Assets.images.dash.bumper.a.inactive.keyName, - ]); - camera.followVector2(Vector2.zero()); await add(DashNestBumper.a()..priority = 1); await traceAllBodies(); 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 5512d4c9..a47b9962 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 @@ -2,11 +2,18 @@ import 'dart:async'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SmallDashNestBumperBGame extends BasicBallGame with Traceable { - static const info = ''' +class SmallDashNestBumperBGame extends BallGame { + SmallDashNestBumperBGame() + : super( + imagesFileNames: [ + Assets.images.dash.bumper.b.active.keyName, + Assets.images.dash.bumper.b.inactive.keyName, + ], + ); + + static const description = ''' Shows how a SmallDashNestBumper ("b") is rendered. - Activate the "trace" parameter to overlay the body. @@ -16,11 +23,6 @@ class SmallDashNestBumperBGame extends BasicBallGame with Traceable { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.dash.bumper.b.active.keyName, - Assets.images.dash.bumper.b.inactive.keyName, - ]); - camera.followVector2(Vector2.zero()); await add(DashNestBumper.b()..priority = 1); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart index a563a09a..ef9c1ffb 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart @@ -1,5 +1,4 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/flutter_forest/big_dash_nest_bumper_game.dart'; import 'package:sandbox/stories/flutter_forest/signpost_game.dart'; @@ -8,39 +7,24 @@ import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dar void addDashNestBumperStories(Dashbook dashbook) { dashbook.storiesOf('Flutter Forest') - ..add( - 'Signpost', - (context) => GameWidget( - game: SignpostGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flutter_forest/signpost.dart'), - info: SignpostGame.info, + ..addGame( + title: 'Signpost', + description: SignpostGame.description, + gameBuilder: (_) => SignpostGame(), ) - ..add( - 'Big Dash Nest Bumper', - (context) => GameWidget( - game: BigDashNestBumperGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flutter_forest/big_dash_nest_bumper.dart'), - info: BigDashNestBumperGame.info, + ..addGame( + title: 'Big Dash Nest Bumper', + description: BigDashNestBumperGame.description, + gameBuilder: (_) => BigDashNestBumperGame(), ) - ..add( - 'Small Dash Nest Bumper A', - (context) => GameWidget( - game: SmallDashNestBumperAGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flutter_forest/small_dash_nest_bumper_a.dart'), - info: SmallDashNestBumperAGame.info, + ..addGame( + title: 'Small Dash Nest Bumper A', + description: SmallDashNestBumperAGame.description, + gameBuilder: (_) => SmallDashNestBumperAGame(), ) - ..add( - 'Small Dash Nest Bumper B', - (context) => GameWidget( - game: SmallDashNestBumperBGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flutter_forest/small_dash_nest_bumper_b.dart'), - info: SmallDashNestBumperBGame.info, + ..addGame( + title: 'Small Dash Nest Bumper B', + description: SmallDashNestBumperBGame.description, + gameBuilder: (_) => SmallDashNestBumperBGame(), ); } 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 index ad6e556b..d8022e57 100644 --- 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 @@ -5,10 +5,10 @@ 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 { +class GoogleLetterGame extends BallGame { GoogleLetterGame() : super(color: const Color(0xFF009900)); - static const info = ''' + static const description = ''' Shows how a GoogleLetter is rendered. - Tap anywhere on the screen to spawn a ball into the game. diff --git a/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart b/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart index 290bf9dd..858a9a30 100644 --- a/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart @@ -1,15 +1,11 @@ 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, + dashbook.storiesOf('Google Word').addGame( + title: 'Letter 0', + description: GoogleLetterGame.description, + gameBuilder: (_) => GoogleLetterGame(), ); } 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 add7b205..9b7d96cc 100644 --- a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart @@ -1,10 +1,9 @@ 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 KickerGame extends BasicBallGame with Traceable { - static const info = ''' +class KickerGame extends BallGame { + static const description = ''' Shows how Kickers are rendered. - Activate the "trace" parameter to overlay the body. @@ -16,14 +15,14 @@ class KickerGame extends BasicBallGame with Traceable { await super.onLoad(); final center = screenToWorld(camera.viewport.canvasSize! / 2); - - final leftKicker = Kicker(side: BoardSide.left) - ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y); - await add(leftKicker); - - final rightKicker = Kicker(side: BoardSide.right) - ..initialPosition = Vector2(center.x + (Kicker.size.x * 2), center.y); - await add(rightKicker); + await addAll( + [ + Kicker(side: BoardSide.left) + ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y), + Kicker(side: BoardSide.right) + ..initialPosition = Vector2(center.x + (Kicker.size.x * 2), center.y), + ], + ); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart index 77d6ff29..cfebb7e4 100644 --- a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/kicker/kicker_game.dart'; void addKickerStories(Dashbook dashbook) { - dashbook.storiesOf('Kickers').add( - 'Basic', - (context) => GameWidget( - game: KickerGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('kicker_game/basic.dart'), - info: KickerGame.info, + dashbook.storiesOf('Kickers').addGame( + title: 'Traced', + description: KickerGame.description, + gameBuilder: (_) => KickerGame(), ); } 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 index 2d636e30..1be94133 100644 --- 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 @@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class LaunchRampGame extends BasicBallGame { +class LaunchRampGame extends BallGame { LaunchRampGame() : super( color: Colors.blue, @@ -14,7 +14,7 @@ class LaunchRampGame extends BasicBallGame { ballLayer: Layer.launcher, ); - static const info = ''' + static const description = ''' Shows how LaunchRamp are rendered. - Activate the "trace" parameter to overlay the body. @@ -28,10 +28,8 @@ class LaunchRampGame extends BasicBallGame { camera ..followVector2(Vector2(0, 0)) ..zoom = 7.5; - - final launchRamp = LaunchRamp(); - unawaited(addFromBlueprint(launchRamp)); - + await addFromBlueprint(LaunchRamp()); + await ready(); 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 index 083a4584..e02ec093 100644 --- a/packages/pinball_components/sandbox/lib/stories/launch_ramp/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/launch_ramp/stories.dart @@ -1,15 +1,11 @@ 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, + dashbook.storiesOf('LaunchRamp').addGame( + title: 'Traced', + description: LaunchRampGame.description, + gameBuilder: (_) => LaunchRampGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart b/packages/pinball_components/sandbox/lib/stories/layer/layer_game.dart similarity index 61% rename from packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart rename to packages/pinball_components/sandbox/lib/stories/layer/layer_game.dart index 60d72ff3..40dcc824 100644 --- a/packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/layer/layer_game.dart @@ -2,36 +2,35 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class BasicLayerGame extends BasicGame with TapDetector { - BasicLayerGame({required this.color}); - - static const info = ''' +class LayerGame extends BallGame with TapDetector { + static const description = ''' Shows how Layers work when a Ball hits other components. - Tap anywhere on the screen to spawn a Ball into the game. '''; - final Color color; - @override Future onLoad() async { - await add(BigSquare()..initialPosition = Vector2(30, -40)); - await add(SmallSquare()..initialPosition = Vector2(50, -40)); - await add(UnlayeredSquare()..initialPosition = Vector2(60, -40)); - } - - @override - void onTapUp(TapUpInfo info) { - add( - Ball(baseColor: color)..initialPosition = info.eventPosition.game, + await addAll( + [ + _BigSquare()..initialPosition = Vector2(30, -40), + _SmallSquare()..initialPosition = Vector2(50, -40), + _UnlayeredSquare()..initialPosition = Vector2(60, -40), + ], ); } } -class BigSquare extends BodyComponent with InitialPosition, Layered { - BigSquare() { +class _BigSquare extends BodyComponent with InitialPosition, Layered { + _BigSquare() + : super( + children: [ + _UnlayeredSquare()..initialPosition = Vector2.all(4), + _SmallSquare()..initialPosition = Vector2.all(-4), + ], + ) { paint = Paint() ..color = const Color.fromARGB(255, 8, 218, 241) ..style = PaintingStyle.stroke; @@ -42,27 +41,13 @@ class BigSquare extends BodyComponent with InitialPosition, Layered { Body createBody() { final shape = PolygonShape()..setAsBoxXY(16, 16); final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef()..position = initialPosition; - return world.createBody(bodyDef)..createFixture(fixtureDef); } - - @override - Future onLoad() async { - await super.onLoad(); - - await addAll( - [ - UnlayeredSquare()..initialPosition = Vector2.all(4), - SmallSquare()..initialPosition = Vector2.all(-4), - ], - ); - } } -class SmallSquare extends BodyComponent with InitialPosition, Layered { - SmallSquare() { +class _SmallSquare extends BodyComponent with InitialPosition, Layered { + _SmallSquare() { paint = Paint() ..color = const Color.fromARGB(255, 27, 241, 8) ..style = PaintingStyle.stroke; @@ -73,15 +58,13 @@ class SmallSquare extends BodyComponent with InitialPosition, Layered { Body createBody() { final shape = PolygonShape()..setAsBoxXY(2, 2); final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef()..position = initialPosition; - return world.createBody(bodyDef)..createFixture(fixtureDef); } } -class UnlayeredSquare extends BodyComponent with InitialPosition { - UnlayeredSquare() { +class _UnlayeredSquare extends BodyComponent with InitialPosition { + _UnlayeredSquare() { paint = Paint() ..color = const Color.fromARGB(255, 241, 8, 8) ..style = PaintingStyle.stroke; @@ -91,9 +74,7 @@ class UnlayeredSquare extends BodyComponent with InitialPosition { Body createBody() { final shape = PolygonShape()..setAsBoxXY(3, 3); final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef()..position = initialPosition; - return world.createBody(bodyDef)..createFixture(fixtureDef); } } diff --git a/packages/pinball_components/sandbox/lib/stories/layer/stories.dart b/packages/pinball_components/sandbox/lib/stories/layer/stories.dart index 12ac028b..64d21b87 100644 --- a/packages/pinball_components/sandbox/lib/stories/layer/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/layer/stories.dart @@ -1,18 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; -import 'package:flutter/material.dart'; import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/layer/basic_layer_game.dart'; +import 'package:sandbox/stories/layer/layer_game.dart'; void addLayerStories(Dashbook dashbook) { - dashbook.storiesOf('Layer').add( - 'Layer', - (context) => GameWidget( - game: BasicLayerGame( - color: context.colorProperty('color', Colors.blue), - ), - ), - codeLink: buildSourceLink('layer/basic.dart'), - info: BasicLayerGame.info, + dashbook.storiesOf('Layer').addGame( + title: 'Example', + description: LayerGame.description, + gameBuilder: (_) => LayerGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart index baaab21b..29eded8c 100644 --- a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -5,10 +5,10 @@ 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 { +class PlungerGame extends BallGame with KeyboardEvents, Traceable { PlungerGame() : super(color: const Color(0xFFFF0000)); - static const info = ''' + static const description = ''' Shows how Plunger is rendered. - Activate the "trace" parameter to overlay the body. @@ -27,11 +27,10 @@ class PlungerGame extends BasicBallGame with KeyboardEvents, Traceable { 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 add( + plunger = Plunger(compressionDistance: 29) + ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y), + ); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart b/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart index 86061dc2..af54435a 100644 --- a/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart @@ -1,15 +1,11 @@ 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, + dashbook.storiesOf('Plunger').addGame( + title: 'Traced', + description: PlungerGame.description, + gameBuilder: (_) => PlungerGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/score_text/basic.dart b/packages/pinball_components/sandbox/lib/stories/score_text/score_text_game.dart similarity index 88% rename from packages/pinball_components/sandbox/lib/stories/score_text/basic.dart rename to packages/pinball_components/sandbox/lib/stories/score_text/score_text_game.dart index 49b83863..aa776405 100644 --- a/packages/pinball_components/sandbox/lib/stories/score_text/basic.dart +++ b/packages/pinball_components/sandbox/lib/stories/score_text/score_text_game.dart @@ -5,8 +5,8 @@ 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 = ''' +class ScoreTextGame extends AssetsGame with TapDetector { + static const description = ''' Simple game to show how score text works, - Tap anywhere on the screen to spawn an text on the given location. diff --git a/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart b/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart index 85caef1b..c4899a27 100644 --- a/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/score_text/basic.dart'; +import 'package:sandbox/stories/score_text/score_text_game.dart'; void addScoreTextStories(Dashbook dashbook) { - dashbook.storiesOf('ScoreText').add( - 'Basic', - (context) => GameWidget( - game: ScoreTextBasicGame(), - ), - codeLink: buildSourceLink('score_text/basic.dart'), - info: ScoreTextBasicGame.info, + dashbook.storiesOf('ScoreText').addGame( + title: 'Basic', + description: ScoreTextGame.description, + gameBuilder: (_) => ScoreTextGame(), ); } 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 a0f54ea4..28858088 100644 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart @@ -1,11 +1,18 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SlingshotGame extends BasicBallGame with Traceable { - static const info = ''' +class SlingshotGame extends BallGame { + SlingshotGame() + : super( + imagesFileNames: [ + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + ], + ); + + static const description = ''' Shows how Slingshots are rendered. - Activate the "trace" parameter to overlay the body. @@ -16,13 +23,9 @@ class SlingshotGame extends BasicBallGame with Traceable { Future onLoad() async { await super.onLoad(); - await images.loadAll([ - Assets.images.slingshot.upper.keyName, - Assets.images.slingshot.lower.keyName, - ]); - - await addFromBlueprint(Slingshots()); camera.followVector2(Vector2.zero()); + await addFromBlueprint(Slingshots()); + await ready(); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart index 70dfa021..e4c04a0f 100644 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/slingshot/slingshot_game.dart'; void addSlingshotStories(Dashbook dashbook) { - dashbook.storiesOf('Slingshots').add( - 'Basic', - (context) => GameWidget( - game: SlingshotGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('slingshot_game/basic.dart'), - info: SlingshotGame.info, + dashbook.storiesOf('Slingshots').addGame( + title: 'Traced', + description: SlingshotGame.description, + gameBuilder: (_) => SlingshotGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship/stories.dart b/packages/pinball_components/sandbox/lib/stories/spaceship/stories.dart deleted file mode 100644 index ac7720a0..00000000 --- a/packages/pinball_components/sandbox/lib/stories/spaceship/stories.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/spaceship/basic_spaceship_game.dart'; - -void addSpaceshipStories(Dashbook dashbook) { - dashbook.storiesOf('Spaceship').add( - 'Basic', - (context) => GameWidget( - game: BasicSpaceshipGame(), - ), - codeLink: buildSourceLink('spaceship/basic.dart'), - info: BasicSpaceshipGame.info, - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_rail/stories.dart b/packages/pinball_components/sandbox/lib/stories/spaceship_rail/stories.dart deleted file mode 100644 index e69ed1db..00000000 --- a/packages/pinball_components/sandbox/lib/stories/spaceship_rail/stories.dart +++ /dev/null @@ -1,16 +0,0 @@ -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/stories.dart b/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/stories.dart deleted file mode 100644 index f0aeadff..00000000 --- a/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/stories.dart +++ /dev/null @@ -1,16 +0,0 @@ -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/sparky_bumper/sparky_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart index 265b6246..9bebde9e 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 @@ -2,11 +2,10 @@ 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 with Traceable { - static const info = ''' +class SparkyBumperGame extends BallGame { + static const description = ''' Shows how a SparkyBumper is rendered. - Activate the "trace" parameter to overlay the body. diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart index 1a5f8801..418636db 100644 --- a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart @@ -1,15 +1,11 @@ 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, + dashbook.storiesOf('Sparky Bumpers').addGame( + title: 'Traced', + description: SparkyBumperGame.description, + gameBuilder: (_) => SparkyBumperGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 338ca384..d8103b4d 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -4,6 +4,7 @@ export 'ball/stories.dart'; export 'baseboard/stories.dart'; export 'boundaries/stories.dart'; export 'chrome_dino/stories.dart'; +export 'dino_wall/stories.dart'; export 'effects/stories.dart'; export 'flipper/stories.dart'; export 'flutter_forest/stories.dart'; @@ -13,8 +14,4 @@ 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'; -export 'spaceship_ramp/stories.dart'; export 'sparky_bumper/stories.dart'; -export 'zoom/stories.dart'; diff --git a/packages/pinball_components/sandbox/lib/stories/zoom/stories.dart b/packages/pinball_components/sandbox/lib/stories/zoom/stories.dart deleted file mode 100644 index 653d5491..00000000 --- a/packages/pinball_components/sandbox/lib/stories/zoom/stories.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/zoom/basic_zoom_game.dart'; - -void addZoomStories(Dashbook dashbook) { - dashbook.storiesOf('CameraZoom').add( - 'Basic', - (context) => GameWidget( - game: BasicCameraZoomGame(), - ), - codeLink: buildSourceLink('zoom/basic_zoom_game.dart'), - info: BasicCameraZoomGame.info, - ); -} diff --git a/packages/pinball_components/test/src/components/boundaries_test.dart b/packages/pinball_components/test/src/components/boundaries_test.dart index 9affe0b7..4e2fb497 100644 --- a/packages/pinball_components/test/src/components/boundaries_test.dart +++ b/packages/pinball_components/test/src/components/boundaries_test.dart @@ -10,20 +10,24 @@ import '../../helpers/helpers.dart'; void main() { group('Boundaries', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ Assets.images.boundary.outer.keyName, + Assets.images.boundary.outerBottom.keyName, Assets.images.boundary.bottom.keyName, ]; - final tester = FlameTester(TestGame.new); + final flameTester = FlameTester(TestGame.new); - tester.testGameWidget( + flameTester.testGameWidget( 'render correctly', setUp: (game, tester) async { await game.images.loadAll(assets); await game.addFromBlueprint(Boundaries()); + await game.ready(); + game.camera.followVector2(Vector2.zero()); game.camera.zoom = 3.2; - await tester.pump(); }, verify: (game, tester) async { await expectLater( diff --git a/packages/pinball_components/test/src/components/dash_animatronic_test.dart b/packages/pinball_components/test/src/components/dash_animatronic_test.dart index 8b5d0e30..d0707223 100644 --- a/packages/pinball_components/test/src/components/dash_animatronic_test.dart +++ b/packages/pinball_components/test/src/components/dash_animatronic_test.dart @@ -22,7 +22,9 @@ void main() { await tester.pump(); }, verify: (game, tester) async { - const animationDuration = 3.25; + final animationDuration = + game.firstChild()!.animation!.totalDuration(); + await expectLater( find.byGame(), matchesGoldenFile('golden/dash_animatronic/start.png'), @@ -60,8 +62,7 @@ void main() { await game.ensureAdd(dashAnimatronic); dashAnimatronic.playing = true; - dashAnimatronic.animation?.setToLast(); - game.update(1); + game.update(4); expect(dashAnimatronic.playing, isFalse); }, diff --git a/packages/pinball_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart index b3a58264..7ed97248 100644 --- a/packages/pinball_components/test/src/components/dino_walls_test.dart +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -11,15 +11,23 @@ import '../../helpers/helpers.dart'; void main() { group('DinoWalls', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.dino.dinoLandTop.keyName, + Assets.images.dino.dinoLandBottom.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { + await game.images.loadAll(assets); await game.addFromBlueprint(DinoWalls()); + await game.ready(); + game.camera.followVector2(Vector2.zero()); game.camera.zoom = 6.5; - await game.ready(); + + await tester.pump(); }, verify: (game, tester) async { await expectLater( diff --git a/packages/pinball_components/test/src/components/golden/boundaries.png b/packages/pinball_components/test/src/components/golden/boundaries.png index ecb396c4..2612679a 100644 Binary files a/packages/pinball_components/test/src/components/golden/boundaries.png and b/packages/pinball_components/test/src/components/golden/boundaries.png differ diff --git a/packages/pinball_components/test/src/components/golden/dash_animatronic/end.png b/packages/pinball_components/test/src/components/golden/dash_animatronic/end.png index 3e5e91f5..c8218fe1 100644 Binary files a/packages/pinball_components/test/src/components/golden/dash_animatronic/end.png and b/packages/pinball_components/test/src/components/golden/dash_animatronic/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/dino-walls.png b/packages/pinball_components/test/src/components/golden/dino-walls.png index 8c2ee569..5956b43b 100644 Binary files a/packages/pinball_components/test/src/components/golden/dino-walls.png and b/packages/pinball_components/test/src/components/golden/dino-walls.png differ diff --git a/packages/pinball_components/test/src/components/golden/sparky-computer.png b/packages/pinball_components/test/src/components/golden/sparky-computer.png index 109f9903..165a79da 100644 Binary files a/packages/pinball_components/test/src/components/golden/sparky-computer.png and b/packages/pinball_components/test/src/components/golden/sparky-computer.png differ diff --git a/packages/pinball_components/test/src/components/golden/sparky_animatronic/end.png b/packages/pinball_components/test/src/components/golden/sparky_animatronic/end.png new file mode 100644 index 00000000..5e963f14 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/sparky_animatronic/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/sparky_animatronic/middle.png b/packages/pinball_components/test/src/components/golden/sparky_animatronic/middle.png new file mode 100644 index 00000000..2665c5cb Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/sparky_animatronic/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/sparky_animatronic/start.png b/packages/pinball_components/test/src/components/golden/sparky_animatronic/start.png new file mode 100644 index 00000000..ea3e6344 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/sparky_animatronic/start.png differ diff --git a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart index 98a6e95f..a65ba18b 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart @@ -29,9 +29,12 @@ void main() { 'loads correctly', (game) async { final spaceshipRamp = SpaceshipRamp(); - await game.ensureAdd(spaceshipRamp); + await game.addFromBlueprint(spaceshipRamp); + await game.ready(); - expect(game.contains(spaceshipRamp), isTrue); + for (final component in spaceshipRamp.components) { + expect(game.contains(component), isTrue); + } }, ); diff --git a/packages/pinball_components/test/src/components/sparky_animatronic_test.dart b/packages/pinball_components/test/src/components/sparky_animatronic_test.dart new file mode 100644 index 00000000..66c2e0a6 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_animatronic_test.dart @@ -0,0 +1,76 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/extensions.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(); + + group('SparkyAnimatronic', () { + final asset = Assets.images.sparky.animatronic.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.load(asset); + await game.ensureAdd(SparkyAnimatronic()..playing = true); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + final animationDuration = + game.firstChild()!.animation!.totalDuration(); + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/sparky_animatronic/start.png'), + ); + + game.update(animationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/sparky_animatronic/middle.png'), + ); + + game.update(animationDuration * 0.75); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/sparky_animatronic/end.png'), + ); + }, + ); + + flameTester.test( + 'loads correctly', + (game) async { + final sparkyAnimatronic = SparkyAnimatronic(); + await game.ensureAdd(sparkyAnimatronic); + + expect(game.contains(sparkyAnimatronic), isTrue); + }, + ); + + flameTester.test( + 'stops animating after animation completes', + (game) async { + final sparkyAnimatronic = SparkyAnimatronic(); + await game.ensureAdd(sparkyAnimatronic); + + sparkyAnimatronic.playing = true; + final animationDuration = + game.firstChild()!.animation!.totalDuration(); + game.update(animationDuration); + + expect(sparkyAnimatronic.playing, isFalse); + }, + ); + }); +} diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart index 2d2e760b..709e7627 100644 --- a/packages/pinball_flame/lib/pinball_flame.dart +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -3,3 +3,4 @@ library pinball_flame; export 'src/blueprint.dart'; export 'src/component_controller.dart'; export 'src/keyboard_input_controller.dart'; +export 'src/sprite_animation.dart'; diff --git a/packages/pinball_flame/lib/src/blueprint.dart b/packages/pinball_flame/lib/src/blueprint.dart index de9c8b2c..c7bd5a5e 100644 --- a/packages/pinball_flame/lib/src/blueprint.dart +++ b/packages/pinball_flame/lib/src/blueprint.dart @@ -1,16 +1,13 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; -// TODO(erickzanardo): Keeping this inside our code base -// so we can experiment with the idea, but this is a -// potential upstream change on Flame. +// TODO(erickzanardo): Keeping this inside our code base so we can experiment +// with the idea, but this is a potential upstream change on Flame. /// {@template blueprint} -/// A [Blueprint] is a virtual way of grouping [Component]s that are related, -/// but they need to be added directly on the [FlameGame] level. +/// A [Blueprint] is a virtual way of grouping [Component]s that are related. /// {@endtemplate blueprint} -// TODO(alestiago): refactor with feat/make-blueprint-extend-component. -class Blueprint extends Component { +class Blueprint { /// {@macro blueprint} Blueprint({ Iterable? components, @@ -27,7 +24,7 @@ class Blueprint extends Component { final List _components = []; - final List _blueprints = []; + final List _blueprints = []; Future _addToParent(Component parent) async { await parent.addAll(_components); @@ -37,7 +34,7 @@ class Blueprint extends Component { List get components => List.unmodifiable(_components); /// Returns a copy of the blueprints built by this blueprint. - List get blueprints => List.unmodifiable(_blueprints); + List get blueprints => List.unmodifiable(_blueprints); } /// Adds helper methods regarding [Blueprint]s to [FlameGame]. diff --git a/packages/pinball_flame/lib/src/sprite_animation.dart b/packages/pinball_flame/lib/src/sprite_animation.dart new file mode 100644 index 00000000..2990fb14 --- /dev/null +++ b/packages/pinball_flame/lib/src/sprite_animation.dart @@ -0,0 +1,102 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/image_composition.dart'; +import 'package:flutter/material.dart' hide Animation; + +/// {@template flame.widgets.sprite_animation_widget} +/// A [StatelessWidget] that renders a [SpriteAnimation]. +/// {@endtemplate} +// TODO(arturplaczek): Remove when this PR will be merged. +// https://github.com/flame-engine/flame/pull/1552 +class SpriteAnimationWidget extends StatelessWidget { + /// {@macro flame.widgets.sprite_animation_widget} + const SpriteAnimationWidget({ + required this.controller, + this.anchor = Anchor.topLeft, + Key? key, + }) : super(key: key); + + /// The positioning [Anchor]. + final Anchor anchor; + + final SpriteAnimationController controller; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (_, __) { + return CustomPaint( + painter: SpritePainter( + controller.animation.getSprite(), + anchor, + ), + ); + }, + ); + } +} + +class SpriteAnimationController extends AnimationController { + SpriteAnimationController({ + required TickerProvider vsync, + required this.animation, + }) : super(vsync: vsync) { + duration = Duration(seconds: animation.totalDuration().ceil()); + } + + final SpriteAnimation animation; + + double? _lastUpdated; + + @override + void notifyListeners() { + super.notifyListeners(); + + final now = DateTime.now().millisecond.toDouble(); + final dt = max(0, (now - (_lastUpdated ?? 0)) / 1000); + animation.update(dt); + _lastUpdated = now; + } +} + +class SpritePainter extends CustomPainter { + SpritePainter( + this._sprite, + this._anchor, { + double angle = 0, + }) : _angle = angle; + + final Sprite _sprite; + final Anchor _anchor; + final double _angle; + + @override + bool shouldRepaint(SpritePainter oldDelegate) { + return oldDelegate._sprite != _sprite || + oldDelegate._anchor != _anchor || + oldDelegate._angle != _angle; + } + + @override + void paint(Canvas canvas, Size size) { + final boxSize = size.toVector2(); + final rate = boxSize.clone()..divide(_sprite.srcSize); + final minRate = min(rate.x, rate.y); + final paintSize = _sprite.srcSize * minRate; + final anchorPosition = _anchor.toVector2(); + final boxAnchorPosition = boxSize.clone()..multiply(anchorPosition); + final spriteAnchorPosition = anchorPosition..multiply(paintSize); + + canvas + ..translateVector(boxAnchorPosition..sub(spriteAnchorPosition)) + ..renderRotated( + _angle, + spriteAnchorPosition, + (canvas) => _sprite.render(canvas, size: paintSize), + ); + } +} diff --git a/packages/pinball_flame/test/src/sprite_animation_test.dart b/packages/pinball_flame/test/src/sprite_animation_test.dart new file mode 100644 index 00000000..e3b287de --- /dev/null +++ b/packages/pinball_flame/test/src/sprite_animation_test.dart @@ -0,0 +1,74 @@ +import 'package:flame/components.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class MockSpriteAnimationController extends Mock + implements SpriteAnimationController {} + +class MockSpriteAnimation extends Mock implements SpriteAnimation {} + +class MockSprite extends Mock implements Sprite {} + +// TODO(arturplaczek): Remove when this PR will be merged. +// https://github.com/flame-engine/flame/pull/1552 + +void main() { + group('PinballSpriteAnimationWidget', () { + late SpriteAnimationController controller; + late SpriteAnimation animation; + late Sprite sprite; + + setUp(() { + controller = MockSpriteAnimationController(); + animation = MockSpriteAnimation(); + sprite = MockSprite(); + + when(() => controller.animation).thenAnswer((_) => animation); + + when(() => animation.totalDuration()).thenAnswer((_) => 1); + when(() => animation.getSprite()).thenAnswer((_) => sprite); + when(() => sprite.srcSize).thenAnswer((_) => Vector2(1, 1)); + when(() => sprite.srcSize).thenAnswer((_) => Vector2(1, 1)); + }); + + testWidgets('renders correctly', (tester) async { + await tester.pumpWidget( + SpriteAnimationWidget( + controller: controller, + ), + ); + + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + + expect(find.byType(SpriteAnimationWidget), findsOneWidget); + }); + + test('SpriteAnimationController is updating animations', () { + SpriteAnimationController( + vsync: const TestVSync(), + animation: animation, + ).notifyListeners(); + + verify(() => animation.update(any())).called(1); + }); + + testWidgets('SpritePainter shouldRepaint returns true when Sprite changed', + (tester) async { + final spritePainter = SpritePainter( + sprite, + Anchor.center, + angle: 45, + ); + + final anotherPainter = SpritePainter( + sprite, + Anchor.center, + angle: 30, + ); + + expect(spritePainter.shouldRepaint(anotherPainter), isTrue); + }); + }); +} diff --git a/packages/pinball_theme/assets/images/android/animation.png b/packages/pinball_theme/assets/images/android/animation.png new file mode 100644 index 00000000..fc7465be Binary files /dev/null and b/packages/pinball_theme/assets/images/android/animation.png differ diff --git a/packages/pinball_theme/assets/images/android/character.png b/packages/pinball_theme/assets/images/android/character.png deleted file mode 100644 index 736f1e6f..00000000 Binary files a/packages/pinball_theme/assets/images/android/character.png and /dev/null differ diff --git a/packages/pinball_theme/assets/images/dash/animation.png b/packages/pinball_theme/assets/images/dash/animation.png new file mode 100644 index 00000000..e812415f Binary files /dev/null and b/packages/pinball_theme/assets/images/dash/animation.png differ diff --git a/packages/pinball_theme/assets/images/dash/character.png b/packages/pinball_theme/assets/images/dash/character.png deleted file mode 100644 index d76795aa..00000000 Binary files a/packages/pinball_theme/assets/images/dash/character.png and /dev/null differ diff --git a/packages/pinball_theme/assets/images/dino/animation.png b/packages/pinball_theme/assets/images/dino/animation.png new file mode 100644 index 00000000..c75b16f9 Binary files /dev/null and b/packages/pinball_theme/assets/images/dino/animation.png differ diff --git a/packages/pinball_theme/assets/images/dino/character.png b/packages/pinball_theme/assets/images/dino/character.png deleted file mode 100644 index c6f5a390..00000000 Binary files a/packages/pinball_theme/assets/images/dino/character.png and /dev/null differ diff --git a/packages/pinball_theme/assets/images/sparky/animation.png b/packages/pinball_theme/assets/images/sparky/animation.png new file mode 100644 index 00000000..1aff4772 Binary files /dev/null and b/packages/pinball_theme/assets/images/sparky/animation.png differ diff --git a/packages/pinball_theme/assets/images/sparky/character.png b/packages/pinball_theme/assets/images/sparky/character.png deleted file mode 100644 index c37403b3..00000000 Binary files a/packages/pinball_theme/assets/images/sparky/character.png and /dev/null differ diff --git a/packages/pinball_theme/lib/pinball_theme.dart b/packages/pinball_theme/lib/pinball_theme.dart index 139a70dc..c8f9b53e 100644 --- a/packages/pinball_theme/lib/pinball_theme.dart +++ b/packages/pinball_theme/lib/pinball_theme.dart @@ -1,5 +1,4 @@ library pinball_theme; export 'src/generated/generated.dart'; -export 'src/pinball_theme.dart'; export 'src/themes/themes.dart'; diff --git a/packages/pinball_theme/lib/src/generated/assets.gen.dart b/packages/pinball_theme/lib/src/generated/assets.gen.dart index f050c621..3feeecce 100644 --- a/packages/pinball_theme/lib/src/generated/assets.gen.dart +++ b/packages/pinball_theme/lib/src/generated/assets.gen.dart @@ -28,6 +28,10 @@ class $AssetsImagesGen { class $AssetsImagesAndroidGen { const $AssetsImagesAndroidGen(); + /// File path: assets/images/android/animation.png + AssetGenImage get animation => + const AssetGenImage('assets/images/android/animation.png'); + /// File path: assets/images/android/background.png AssetGenImage get background => const AssetGenImage('assets/images/android/background.png'); @@ -48,6 +52,10 @@ class $AssetsImagesAndroidGen { class $AssetsImagesDashGen { const $AssetsImagesDashGen(); + /// File path: assets/images/dash/animation.png + AssetGenImage get animation => + const AssetGenImage('assets/images/dash/animation.png'); + /// File path: assets/images/dash/background.png AssetGenImage get background => const AssetGenImage('assets/images/dash/background.png'); @@ -67,6 +75,10 @@ class $AssetsImagesDashGen { class $AssetsImagesDinoGen { const $AssetsImagesDinoGen(); + /// File path: assets/images/dino/animation.png + AssetGenImage get animation => + const AssetGenImage('assets/images/dino/animation.png'); + /// File path: assets/images/dino/background.png AssetGenImage get background => const AssetGenImage('assets/images/dino/background.png'); @@ -86,6 +98,10 @@ class $AssetsImagesDinoGen { class $AssetsImagesSparkyGen { const $AssetsImagesSparkyGen(); + /// File path: assets/images/sparky/animation.png + AssetGenImage get animation => + const AssetGenImage('assets/images/sparky/animation.png'); + /// File path: assets/images/sparky/background.png AssetGenImage get background => const AssetGenImage('assets/images/sparky/background.png'); diff --git a/packages/pinball_theme/lib/src/pinball_theme.dart b/packages/pinball_theme/lib/src/pinball_theme.dart deleted file mode 100644 index a766a129..00000000 --- a/packages/pinball_theme/lib/src/pinball_theme.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -/// {@template pinball_theme} -/// Defines all theme assets and attributes. -/// -/// Game components should have a getter specified here to load their -/// corresponding assets for the game. -/// {@endtemplate} -class PinballTheme extends Equatable { - /// {@macro pinball_theme} - const PinballTheme({ - required CharacterTheme characterTheme, - }) : _characterTheme = characterTheme; - - final CharacterTheme _characterTheme; - - /// [CharacterTheme] for the chosen character. - CharacterTheme get characterTheme => _characterTheme; - - @override - List get props => [_characterTheme]; -} diff --git a/packages/pinball_theme/lib/src/themes/android_theme.dart b/packages/pinball_theme/lib/src/themes/android_theme.dart index 1001ca5f..8989c717 100644 --- a/packages/pinball_theme/lib/src/themes/android_theme.dart +++ b/packages/pinball_theme/lib/src/themes/android_theme.dart @@ -14,9 +14,6 @@ class AndroidTheme extends CharacterTheme { @override Color get ballColor => Colors.green; - @override - AssetGenImage get character => Assets.images.android.character; - @override AssetGenImage get background => Assets.images.android.background; @@ -25,4 +22,7 @@ class AndroidTheme extends CharacterTheme { @override AssetGenImage get leaderboardIcon => Assets.images.android.leaderboardIcon; + + @override + AssetGenImage get animation => Assets.images.android.animation; } diff --git a/packages/pinball_theme/lib/src/themes/character_theme.dart b/packages/pinball_theme/lib/src/themes/character_theme.dart index 9c5f6ba6..072c917f 100644 --- a/packages/pinball_theme/lib/src/themes/character_theme.dart +++ b/packages/pinball_theme/lib/src/themes/character_theme.dart @@ -18,9 +18,6 @@ abstract class CharacterTheme extends Equatable { /// Ball color for this theme. Color get ballColor; - /// Asset for the theme character. - AssetGenImage get character; - /// Asset for the background. AssetGenImage get background; @@ -30,13 +27,16 @@ abstract class CharacterTheme extends Equatable { /// Icon asset for the leaderboard. AssetGenImage get leaderboardIcon; + /// Asset for the the idle character animation. + AssetGenImage get animation; + @override List get props => [ name, ballColor, - character, background, icon, leaderboardIcon, + animation, ]; } diff --git a/packages/pinball_theme/lib/src/themes/dash_theme.dart b/packages/pinball_theme/lib/src/themes/dash_theme.dart index 9038f2fc..7584c8ed 100644 --- a/packages/pinball_theme/lib/src/themes/dash_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dash_theme.dart @@ -14,9 +14,6 @@ class DashTheme extends CharacterTheme { @override Color get ballColor => Colors.blue; - @override - AssetGenImage get character => Assets.images.dash.character; - @override AssetGenImage get background => Assets.images.dash.background; @@ -25,4 +22,7 @@ class DashTheme extends CharacterTheme { @override AssetGenImage get leaderboardIcon => Assets.images.dash.leaderboardIcon; + + @override + AssetGenImage get animation => Assets.images.dash.animation; } diff --git a/packages/pinball_theme/lib/src/themes/dino_theme.dart b/packages/pinball_theme/lib/src/themes/dino_theme.dart index b434463e..3baf466c 100644 --- a/packages/pinball_theme/lib/src/themes/dino_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dino_theme.dart @@ -14,9 +14,6 @@ class DinoTheme extends CharacterTheme { @override Color get ballColor => Colors.grey; - @override - AssetGenImage get character => Assets.images.dino.character; - @override AssetGenImage get background => Assets.images.dino.background; @@ -25,4 +22,7 @@ class DinoTheme extends CharacterTheme { @override AssetGenImage get leaderboardIcon => Assets.images.dino.leaderboardIcon; + + @override + AssetGenImage get animation => Assets.images.dino.animation; } diff --git a/packages/pinball_theme/lib/src/themes/sparky_theme.dart b/packages/pinball_theme/lib/src/themes/sparky_theme.dart index 9e71bef8..7884a22f 100644 --- a/packages/pinball_theme/lib/src/themes/sparky_theme.dart +++ b/packages/pinball_theme/lib/src/themes/sparky_theme.dart @@ -14,9 +14,6 @@ class SparkyTheme extends CharacterTheme { @override String get name => 'Sparky'; - @override - AssetGenImage get character => Assets.images.sparky.character; - @override AssetGenImage get background => Assets.images.sparky.background; @@ -25,4 +22,7 @@ class SparkyTheme extends CharacterTheme { @override AssetGenImage get leaderboardIcon => Assets.images.sparky.leaderboardIcon; + + @override + AssetGenImage get animation => Assets.images.sparky.animation; } diff --git a/packages/pinball_theme/test/src/pinball_theme_test.dart b/packages/pinball_theme/test/src/pinball_theme_test.dart deleted file mode 100644 index 899eec64..00000000 --- a/packages/pinball_theme/test/src/pinball_theme_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -void main() { - group('PinballTheme', () { - const characterTheme = SparkyTheme(); - - test('can be instantiated', () { - expect(PinballTheme(characterTheme: characterTheme), isNotNull); - }); - - test('supports value equality', () { - expect( - PinballTheme(characterTheme: characterTheme), - equals(PinballTheme(characterTheme: characterTheme)), - ); - }); - - test('characterTheme is correct', () { - expect( - PinballTheme(characterTheme: characterTheme).characterTheme, - equals(characterTheme), - ); - }); - }); -} diff --git a/packages/share_repository/.gitignore b/packages/share_repository/.gitignore new file mode 100644 index 00000000..526da158 --- /dev/null +++ b/packages/share_repository/.gitignore @@ -0,0 +1,7 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock \ No newline at end of file diff --git a/packages/share_repository/README.md b/packages/share_repository/README.md new file mode 100644 index 00000000..0473707b --- /dev/null +++ b/packages/share_repository/README.md @@ -0,0 +1,11 @@ +# share_repository + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Repository to facilitate sharing scores. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/share_repository/analysis_options.yaml b/packages/share_repository/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/share_repository/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/share_repository/lib/share_repository.dart b/packages/share_repository/lib/share_repository.dart new file mode 100644 index 00000000..0a68aff4 --- /dev/null +++ b/packages/share_repository/lib/share_repository.dart @@ -0,0 +1,4 @@ +library share_repository; + +export 'src/models/models.dart'; +export 'src/share_repository.dart'; diff --git a/packages/share_repository/lib/src/models/models.dart b/packages/share_repository/lib/src/models/models.dart new file mode 100644 index 00000000..26819946 --- /dev/null +++ b/packages/share_repository/lib/src/models/models.dart @@ -0,0 +1 @@ +export 'share_platform.dart'; diff --git a/packages/share_repository/lib/src/models/share_platform.dart b/packages/share_repository/lib/src/models/share_platform.dart new file mode 100644 index 00000000..054a4f15 --- /dev/null +++ b/packages/share_repository/lib/src/models/share_platform.dart @@ -0,0 +1,8 @@ +/// The platform that is being used to share a score. +enum SharePlatform { + /// Twitter platform. + twitter, + + /// Facebook platform. + facebook, +} diff --git a/packages/share_repository/lib/src/share_repository.dart b/packages/share_repository/lib/src/share_repository.dart new file mode 100644 index 00000000..6e6679c2 --- /dev/null +++ b/packages/share_repository/lib/src/share_repository.dart @@ -0,0 +1,30 @@ +import 'package:share_repository/share_repository.dart'; + +/// {@template share_repository} +/// Repository to facilitate sharing scores. +/// {@endtemplate} +class ShareRepository { + /// {@macro share_repository} + const ShareRepository({ + required String appUrl, + }) : _appUrl = appUrl; + + final String _appUrl; + + /// Returns a url to share the [value] on the given [platform]. + /// + /// The returned url can be opened using the [url_launcher](https://pub.dev/packages/url_launcher) package. + String shareText({ + required String value, + required SharePlatform platform, + }) { + final encodedUrl = Uri.encodeComponent(_appUrl); + final encodedShareText = Uri.encodeComponent(value); + switch (platform) { + case SharePlatform.twitter: + return 'https://twitter.com/intent/tweet?url=$encodedUrl&text=$encodedShareText'; + case SharePlatform.facebook: + return 'https://www.facebook.com/sharer.php?u=$encodedUrl"e=$encodedShareText'; + } + } +} diff --git a/packages/share_repository/pubspec.yaml b/packages/share_repository/pubspec.yaml new file mode 100644 index 00000000..dc3d4e86 --- /dev/null +++ b/packages/share_repository/pubspec.yaml @@ -0,0 +1,13 @@ +name: share_repository +description: Repository to facilitate sharing scores. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dev_dependencies: + coverage: ^1.1.0 + mocktail: ^0.2.0 + test: ^1.19.2 + very_good_analysis: ^2.4.0 diff --git a/packages/share_repository/test/src/share_repository_test.dart b/packages/share_repository/test/src/share_repository_test.dart new file mode 100644 index 00000000..bdb2c517 --- /dev/null +++ b/packages/share_repository/test/src/share_repository_test.dart @@ -0,0 +1,41 @@ +// ignore_for_file: prefer_const_constructors +import 'package:share_repository/share_repository.dart'; +import 'package:test/test.dart'; + +void main() { + group('ShareRepository', () { + const appUrl = 'https://fakeurl.com/'; + late ShareRepository shareRepository; + + setUp(() { + shareRepository = ShareRepository(appUrl: appUrl); + }); + + test('can be instantiated', () { + expect(ShareRepository(appUrl: appUrl), isNotNull); + }); + + group('shareText', () { + const value = 'hello world!'; + test('returns the correct share url for twitter', () async { + const shareTextUrl = + 'https://twitter.com/intent/tweet?url=https%3A%2F%2Ffakeurl.com%2F&text=hello%20world!'; + final shareTextResult = shareRepository.shareText( + value: value, + platform: SharePlatform.twitter, + ); + expect(shareTextResult, equals(shareTextUrl)); + }); + + test('returns the correct share url for facebook', () async { + const shareTextUrl = + 'https://www.facebook.com/sharer.php?u=https%3A%2F%2Ffakeurl.com%2F"e=hello%20world!'; + final shareTextResult = shareRepository.shareText( + value: value, + platform: SharePlatform.facebook, + ); + expect(shareTextResult, equals(shareTextUrl)); + }); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 31aed6af..1a502f37 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "39.0.0" + version: "31.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "2.8.0" args: dependency: transitive description: @@ -71,6 +71,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -314,7 +321,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.6.3" json_annotation: dependency: transitive description: @@ -349,7 +356,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.3" meta: dependency: transitive description: @@ -412,7 +419,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.0" path_provider: dependency: transitive description: @@ -585,7 +592,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -627,21 +634,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.19.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.8" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.9" typed_data: dependency: transitive description: @@ -662,7 +669,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.1" very_good_analysis: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index f17ea07a..3b950c27 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ flutter: assets: - assets/images/components/ - assets/images/bonus_animation/ + - assets/images/score/ flutter_gen: line_length: 80 diff --git a/test/game/components/controlled_sparky_computer_test.dart b/test/game/components/controlled_sparky_computer_test.dart deleted file mode 100644 index 6ba21a44..00000000 --- a/test/game/components/controlled_sparky_computer_test.dart +++ /dev/null @@ -1,38 +0,0 @@ -// ignore_for_file: cascade_invocations - -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 '../../helpers/helpers.dart'; - -void main() { - group('ControlledSparkyComputer', () { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballTestGame.new); - - flameTester.test('loads correctly', (game) async { - final sparkyComputer = ControlledSparkyComputer(); - await game.ensureAdd(sparkyComputer); - expect(game.children, contains(sparkyComputer)); - }); - - flameTester.testGameWidget( - 'SparkyTurboChargeSensorBallContactCallback turbo charges the ball', - setUp: (game, tester) async { - final contackCallback = SparkyComputerSensorBallContactCallback(); - final sparkyTurboChargeSensor = MockSparkyComputerSensor(); - final ball = MockControlledBall(); - final controller = MockBallController(); - - when(() => ball.controller).thenReturn(controller); - when(controller.turboCharge).thenAnswer((_) async {}); - - contackCallback.begin(sparkyTurboChargeSensor, ball, MockContact()); - - verify(() => ball.controller.turboCharge()).called(1); - }, - ); - }); -} diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index 4efc7174..3de04b90 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -57,8 +57,7 @@ void main() { when(game.firstChild).thenReturn(backboard); when(game.firstChild).thenReturn(cameraController); when(() => game.overlays).thenReturn(overlays); - when(() => game.theme) - .thenReturn(PinballTheme(characterTheme: DashTheme())); + when(() => game.characterTheme).thenReturn(DashTheme()); }); test( diff --git a/test/game/components/sparky_fire_zone_test.dart b/test/game/components/sparky_fire_zone_test.dart index 4c3fcdb9..0ad69dab 100644 --- a/test/game/components/sparky_fire_zone_test.dart +++ b/test/game/components/sparky_fire_zone_test.dart @@ -8,6 +8,7 @@ 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 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; @@ -20,29 +21,50 @@ void main() { Assets.images.sparky.bumper.b.inactive.keyName, Assets.images.sparky.bumper.c.active.keyName, Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.animatronic.keyName, ]; final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); group('SparkyFireZone', () { - flameTester.test( - 'loads correctly', - (game) async { - final sparkyFireZone = SparkyFireZone(); - await game.ensureAdd(sparkyFireZone); - - expect(game.contains(sparkyFireZone), isTrue); - }, - ); + flameTester.test('loads correctly', (game) async { + await game.addFromBlueprint(SparkyFireZone()); + await game.ready(); + }); group('loads', () { + flameTester.test( + 'a SparkyComputer', + (game) async { + expect( + SparkyFireZone().blueprints.whereType().single, + isNotNull, + ); + }, + ); + + flameTester.test( + 'a SparkyAnimatronic', + (game) async { + final sparkyFireZone = SparkyFireZone(); + await game.addFromBlueprint(sparkyFireZone); + await game.ready(); + + expect( + game.descendants().whereType().single, + isNotNull, + ); + }, + ); + flameTester.test( 'three SparkyBumper', (game) async { final sparkyFireZone = SparkyFireZone(); - await game.ensureAdd(sparkyFireZone); + await game.addFromBlueprint(sparkyFireZone); + await game.ready(); expect( - sparkyFireZone.descendants().whereType().length, + game.descendants().whereType().length, equals(3), ); }, @@ -84,11 +106,11 @@ void main() { setUp: (game, tester) async { final ball = Ball(baseColor: const Color(0xFF00FFFF)); final sparkyFireZone = SparkyFireZone(); - await game.ensureAdd(sparkyFireZone); + await game.addFromBlueprint(sparkyFireZone); await game.ensureAdd(ball); game.addContactCallback(BallScorePointsCallback(game)); - final bumpers = sparkyFireZone.descendants().whereType(); + final bumpers = sparkyFireZone.components.whereType(); for (final bumper in bumpers) { beginContact(game, bumper, ball); @@ -102,4 +124,40 @@ void main() { ); }); }); + + group('SparkyTurboChargeSensorBallContactCallback', () { + flameTester.test('calls turboCharge', (game) async { + final callback = SparkyComputerSensorBallContactCallback(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + when(() => ball.gameRef).thenReturn(game); + when(controller.turboCharge).thenAnswer((_) async {}); + + callback.begin(MockSparkyComputerSensor(), ball, MockContact()); + + verify(() => ball.controller.turboCharge()).called(1); + }); + + flameTester.test('plays SparkyAnimatronic', (game) async { + final callback = SparkyComputerSensorBallContactCallback(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + when(() => ball.gameRef).thenReturn(game); + when(controller.turboCharge).thenAnswer((_) async {}); + + final sparkyFireZone = SparkyFireZone(); + await game.addFromBlueprint(sparkyFireZone); + await game.ready(); + + final sparkyAnimatronic = + sparkyFireZone.components.whereType().single; + + expect(sparkyAnimatronic.playing, isFalse); + callback.begin(MockSparkyComputerSensor(), ball, MockContact()); + + expect(sparkyAnimatronic.playing, isTrue); + }); + }); } diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 63a39991..92fe33ea 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -117,7 +117,9 @@ void main() { flameBlocTester.testGameWidget( 'when ball is launch', setUp: (game, tester) async { - final ball = ControlledBall.launch(theme: game.theme); + final ball = ControlledBall.launch( + characterTheme: game.characterTheme, + ); final wall = BottomWall(); await game.ensureAddAll([ball, wall]); game.addContactCallback(BottomWallBallContactCallback()); @@ -132,7 +134,9 @@ void main() { flameBlocTester.testGameWidget( 'when ball is bonus', setUp: (game, tester) async { - final ball = ControlledBall.bonus(theme: game.theme); + final ball = ControlledBall.bonus( + characterTheme: game.characterTheme, + ); final wall = BottomWall(); await game.ensureAddAll([ball, wall]); game.addContactCallback(BottomWallBallContactCallback()); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 4c3dbbfb..d6bbb099 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -34,6 +34,7 @@ void main() { Assets.images.sparky.bumper.b.inactive.keyName, Assets.images.sparky.bumper.c.active.keyName, Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.animatronic.keyName, Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.spaceship.ramp.railingBackground.keyName, @@ -49,9 +50,12 @@ void main() { Assets.images.flipper.left.keyName, Assets.images.flipper.right.keyName, Assets.images.boundary.outer.keyName, + Assets.images.boundary.outerBottom.keyName, Assets.images.boundary.bottom.keyName, Assets.images.slingshot.upper.keyName, Assets.images.slingshot.lower.keyName, + Assets.images.dino.dinoLandTop.keyName, + Assets.images.dino.dinoLandBottom.keyName, ]; final flameTester = FlameTester(() => PinballTestGame(assets)); final debugModeFlameTester = FlameTester(() => DebugPinballTestGame(assets)); @@ -92,14 +96,6 @@ void main() { ); }); - flameTester.test( - 'one SparkyFireZone', - (game) async { - await game.ready(); - expect(game.children.whereType().length, equals(1)); - }, - ); - flameTester.test( 'one AlienZone', (game) async { diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart deleted file mode 100644 index cdc56832..00000000 --- a/test/game/view/game_hud_test.dart +++ /dev/null @@ -1,83 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; -import '../../helpers/helpers.dart'; - -void main() { - group('GameHud', () { - late GameBloc gameBloc; - const initialState = GameState( - score: 10, - balls: 2, - bonusHistory: [], - ); - - void _mockState(GameState state) { - whenListen( - gameBloc, - Stream.value(state), - initialState: state, - ); - } - - Future _pumpHud(WidgetTester tester) async { - await tester.pumpApp( - GameHud(), - gameBloc: gameBloc, - ); - } - - setUp(() { - gameBloc = MockGameBloc(); - _mockState(initialState); - }); - - testWidgets( - 'renders the current score', - (tester) async { - await _pumpHud(tester); - expect(find.text(initialState.score.toString()), findsOneWidget); - }, - ); - - testWidgets( - 'renders the current ball number', - (tester) async { - await _pumpHud(tester); - expect( - find.byType(CircleAvatar), - findsNWidgets(initialState.balls), - ); - }, - ); - - testWidgets('updates the score', (tester) async { - await _pumpHud(tester); - expect(find.text(initialState.score.toString()), findsOneWidget); - - _mockState(initialState.copyWith(score: 20)); - - await tester.pump(); - expect(find.text('20'), findsOneWidget); - }); - - testWidgets('updates the ball number', (tester) async { - await _pumpHud(tester); - expect( - find.byType(CircleAvatar), - findsNWidgets(initialState.balls), - ); - - _mockState(initialState.copyWith(balls: 1)); - - await tester.pump(); - expect( - find.byType(CircleAvatar), - findsNWidgets(1), - ); - }); - }); -} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index bbed2963..191d3676 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -5,7 +5,8 @@ import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import '../../helpers/helpers.dart'; @@ -13,18 +14,18 @@ void main() { final game = PinballTestGame(); group('PinballGamePage', () { - late ThemeCubit themeCubit; + late CharacterThemeCubit characterThemeCubit; late GameBloc gameBloc; setUp(() async { await Future.wait(game.preLoadAssets()); - themeCubit = MockThemeCubit(); + characterThemeCubit = MockCharacterThemeCubit(); gameBloc = MockGameBloc(); whenListen( - themeCubit, - const Stream.empty(), - initialState: const ThemeState.initial(), + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), ); whenListen( @@ -37,7 +38,7 @@ void main() { testWidgets('renders PinballGameView', (tester) async { await tester.pumpApp( PinballGamePage(), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); expect(find.byType(PinballGameView), findsOneWidget); @@ -62,7 +63,7 @@ void main() { game: game, ), assetsManagerCubit: assetsManagerCubit, - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); expect( @@ -79,6 +80,7 @@ void main() { 'renders PinballGameLoadedView after resources have been loaded', (tester) async { final assetsManagerCubit = MockAssetsManagerCubit(); + final startGameBloc = MockStartGameBloc(); final loadedAssetsState = AssetsManagerState( loadables: [Future.value()], @@ -89,14 +91,20 @@ void main() { Stream.value(loadedAssetsState), initialState: loadedAssetsState, ); + whenListen( + startGameBloc, + Stream.value(StartGameState.initial()), + initialState: StartGameState.initial(), + ); await tester.pumpApp( PinballGameView( game: game, ), assetsManagerCubit: assetsManagerCubit, - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, gameBloc: gameBloc, + startGameBloc: startGameBloc, ); await tester.pump(); @@ -126,7 +134,7 @@ void main() { }, ), ), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); await tester.tap(find.text('Tap me')); @@ -160,27 +168,59 @@ void main() { }); group('PinballGameView', () { + final gameBloc = MockGameBloc(); + final startGameBloc = MockStartGameBloc(); + setUp(() async { await Future.wait(game.preLoadAssets()); - }); - testWidgets('renders game and a hud', (tester) async { - final gameBloc = MockGameBloc(); whenListen( gameBloc, Stream.value(const GameState.initial()), initialState: const GameState.initial(), ); + whenListen( + startGameBloc, + Stream.value(StartGameState.initial()), + initialState: StartGameState.initial(), + ); + }); + + testWidgets('renders game', (tester) async { await tester.pumpApp( PinballGameView(game: game), gameBloc: gameBloc, + startGameBloc: startGameBloc, ); expect( find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, ); + expect( + find.byType(GameHud), + findsNothing, + ); + }); + + testWidgets('renders a hud on play state', (tester) async { + final startGameState = StartGameState.initial().copyWith( + status: StartGameStatus.play, + ); + + whenListen( + startGameBloc, + Stream.value(startGameState), + initialState: startGameState, + ); + + await tester.pumpApp( + PinballGameView(game: game), + gameBloc: gameBloc, + startGameBloc: startGameBloc, + ); + expect( find.byType(GameHud), findsOneWidget, diff --git a/test/game/view/widgets/bonus_animation_test.dart b/test/game/view/widgets/bonus_animation_test.dart index 9c23ae0d..aa5a5b83 100644 --- a/test/game/view/widgets/bonus_animation_test.dart +++ b/test/game/view/widgets/bonus_animation_test.dart @@ -1,13 +1,13 @@ -import 'dart:async'; +// ignore_for_file: invalid_use_of_protected_member import 'dart:ui' as ui; import 'package:flame/assets.dart'; -import 'package:flame/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/view/widgets/bonus_animation.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import '../../../helpers/helpers.dart'; @@ -15,11 +15,15 @@ class MockImages extends Mock implements Images {} class MockImage extends Mock implements ui.Image {} +class MockCallback extends Mock { + void call(); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() async { - await BonusAnimation.loadAssets(); + await Future.wait(BonusAnimation.loadAssets()); }); group('loads SpriteAnimationWidget correctly for', () { @@ -32,9 +36,9 @@ void main() { expect(find.byType(SpriteAnimationWidget), findsOneWidget); }); - testWidgets('dino', (tester) async { + testWidgets('dinoChomp', (tester) async { await tester.pumpApp( - BonusAnimation.dino(), + BonusAnimation.dinoChomp(), ); await tester.pump(); @@ -50,18 +54,18 @@ void main() { expect(find.byType(SpriteAnimationWidget), findsOneWidget); }); - testWidgets('google', (tester) async { + testWidgets('googleWord', (tester) async { await tester.pumpApp( - BonusAnimation.google(), + BonusAnimation.googleWord(), ); await tester.pump(); expect(find.byType(SpriteAnimationWidget), findsOneWidget); }); - testWidgets('android', (tester) async { + testWidgets('androidSpaceship', (tester) async { await tester.pumpApp( - BonusAnimation.android(), + BonusAnimation.androidSpaceship(), ); await tester.pump(); @@ -74,14 +78,14 @@ void main() { // https://github.com/flame-engine/flame/issues/1543 testWidgets('called onCompleted callback at the end of animation ', (tester) async { - final completer = Completer(); + final callback = MockCallback(); await tester.runAsync(() async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: BonusAnimation.dashNest( - onCompleted: completer.complete, + onCompleted: callback.call, ), ), ), @@ -93,7 +97,63 @@ void main() { await tester.pump(); - expect(completer.isCompleted, isTrue); + verify(callback.call).called(1); + }); + }); + + testWidgets('called onCompleted callback at the end of animation ', + (tester) async { + final callback = MockCallback(); + + await tester.runAsync(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BonusAnimation.dashNest( + onCompleted: callback.call, + ), + ), + ), + ); + + await tester.pump(); + + await Future.delayed(const Duration(seconds: 4)); + + await tester.pump(); + + verify(callback.call).called(1); + }); + }); + + testWidgets('called onCompleted once when animation changed', (tester) async { + final callback = MockCallback(); + final secondAnimation = BonusAnimation.sparkyTurboCharge( + onCompleted: callback.call, + ); + + await tester.runAsync(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BonusAnimation.dashNest( + onCompleted: callback.call, + ), + ), + ), + ); + + await tester.pump(); + + tester + .state(find.byType(BonusAnimation)) + .didUpdateWidget(secondAnimation); + + await Future.delayed(const Duration(seconds: 4)); + + await tester.pump(); + + verify(callback.call).called(1); }); }); } diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart new file mode 100644 index 00000000..f8307b05 --- /dev/null +++ b/test/game/view/widgets/game_hud_test.dart @@ -0,0 +1,137 @@ +// ignore_for_file: prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart'; +import '../../../helpers/helpers.dart'; + +void main() { + group('GameHud', () { + late GameBloc gameBloc; + + const initialState = GameState( + score: 1000, + balls: 2, + bonusHistory: [], + ); + + setUp(() async { + gameBloc = MockGameBloc(); + await Future.wait(BonusAnimation.loadAssets()); + + whenListen( + gameBloc, + Stream.value(initialState), + initialState: initialState, + ); + }); + + // We cannot use pumpApp when we are testing animation because + // animation tests needs to be run and check in tester.runAsync + Future _pumpAppWithWidget(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: BlocProvider.value( + value: gameBloc, + child: GameHud(), + ), + ), + ), + ); + } + + group('renders ScoreView widget', () { + testWidgets( + 'with the score', + (tester) async { + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + + expect(find.text(initialState.score.formatScore()), findsOneWidget); + }, + ); + + testWidgets( + 'on game over', + (tester) async { + final state = initialState.copyWith( + bonusHistory: [GameBonus.dashNest], + balls: 0, + ); + + whenListen( + gameBloc, + Stream.value(state), + initialState: initialState, + ); + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + + expect(find.byType(ScoreView), findsOneWidget); + expect(find.byType(BonusAnimation), findsNothing); + }, + ); + }); + + for (final gameBonus in GameBonus.values) { + testWidgets('renders BonusAnimation for $gameBonus', (tester) async { + await tester.runAsync(() async { + final state = initialState.copyWith( + bonusHistory: [gameBonus], + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: initialState, + ); + + await _pumpAppWithWidget(tester); + await tester.pump(); + + expect(find.byType(BonusAnimation), findsOneWidget); + }); + }); + } + + testWidgets( + 'goes back to ScoreView after the animation', + (tester) async { + await tester.runAsync(() async { + final state = initialState.copyWith( + bonusHistory: [GameBonus.dashNest], + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: initialState, + ); + + await _pumpAppWithWidget(tester); + await tester.pump(); + // TODO(arturplaczek): remove magic number once this is merged: + // https://github.com/flame-engine/flame/pull/1564 + await Future.delayed(const Duration(seconds: 4)); + + await expectLater(find.byType(ScoreView), findsOneWidget); + }); + }, + ); + }); +} diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 210cc347..0345978d 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; import '../../../helpers/helpers.dart'; diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart new file mode 100644 index 00000000..8281ce83 --- /dev/null +++ b/test/game/view/widgets/round_count_display_test.dart @@ -0,0 +1,132 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/app_colors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + group('RoundCountDisplay renders', () { + late GameBloc gameBloc; + const initialState = GameState( + score: 0, + balls: 3, + bonusHistory: [], + ); + + setUp(() { + gameBloc = MockGameBloc(); + + whenListen( + gameBloc, + Stream.value(initialState), + initialState: initialState, + ); + }); + + testWidgets('three active round indicator', (tester) async { + await tester.pumpApp( + const RoundCountDisplay(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.byType(RoundIndicator), findsNWidgets(3)); + }); + + testWidgets('two active round indicator', (tester) async { + final state = initialState.copyWith( + balls: 2, + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp( + const RoundCountDisplay(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && widget.isActive, + ), + findsNWidgets(2), + ); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && !widget.isActive, + ), + findsOneWidget, + ); + }); + + testWidgets('one active round indicator', (tester) async { + final state = initialState.copyWith( + balls: 1, + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp( + const RoundCountDisplay(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && widget.isActive, + ), + findsOneWidget, + ); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && !widget.isActive, + ), + findsNWidgets(2), + ); + }); + }); + + testWidgets('active round indicator is displaying with proper color', + (tester) async { + await tester.pumpApp( + const RoundIndicator(isActive: true), + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is Container && widget.color == AppColors.orange, + ), + findsOneWidget, + ); + }); + + testWidgets('inactive round indicator is displaying with proper color', + (tester) async { + await tester.pumpApp( + const RoundIndicator(isActive: false), + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is Container && + widget.color == AppColors.orange.withAlpha(128), + ), + findsOneWidget, + ); + }); +} diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart new file mode 100644 index 00000000..0d3af694 --- /dev/null +++ b/test/game/view/widgets/score_view_test.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + late GameBloc gameBloc; + late StreamController stateController; + const score = 123456789; + const initialState = GameState( + score: score, + balls: 1, + bonusHistory: [], + ); + + setUp(() { + gameBloc = MockGameBloc(); + stateController = StreamController()..add(initialState); + + whenListen( + gameBloc, + stateController.stream, + initialState: initialState, + ); + }); + + group('ScoreView', () { + testWidgets('renders score', (tester) async { + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.text(score.formatScore()), findsOneWidget); + }); + + testWidgets('renders game over', (tester) async { + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + stateController.add( + initialState.copyWith( + balls: 0, + ), + ); + + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.text(l10n.gameOver), findsOneWidget); + }); + + testWidgets('updates the score', (tester) async { + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + + expect(find.text(score.formatScore()), findsOneWidget); + + final newState = initialState.copyWith( + score: 987654321, + ); + + stateController.add(newState); + + await tester.pump(); + + expect(find.text(newState.score.formatScore()), findsOneWidget); + }); + }); +} diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 9b0f67c9..586ef3b0 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -8,7 +8,8 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -33,9 +34,11 @@ class MockContactCallback extends Mock class MockGameBloc extends Mock implements GameBloc {} +class MockStartGameBloc extends Mock implements StartGameBloc {} + class MockGameState extends Mock implements GameState {} -class MockThemeCubit extends Mock implements ThemeCubit {} +class MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} class MockLeaderboardBloc extends Mock implements LeaderboardBloc {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 92e2c042..2c112426 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -14,7 +14,8 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'helpers.dart'; @@ -51,8 +52,9 @@ extension PumpApp on WidgetTester { Widget widget, { MockNavigator? navigator, GameBloc? gameBloc, + StartGameBloc? startGameBloc, AssetsManagerCubit? assetsManagerCubit, - ThemeCubit? themeCubit, + CharacterThemeCubit? characterThemeCubit, LeaderboardRepository? leaderboardRepository, PinballAudio? pinballAudio, }) { @@ -70,11 +72,14 @@ extension PumpApp on WidgetTester { child: MultiBlocProvider( providers: [ BlocProvider.value( - value: themeCubit ?? MockThemeCubit(), + value: characterThemeCubit ?? MockCharacterThemeCubit(), ), BlocProvider.value( value: gameBloc ?? MockGameBloc(), ), + BlocProvider.value( + value: startGameBloc ?? MockStartGameBloc(), + ), BlocProvider.value( value: assetsManagerCubit ?? _buildDefaultAssetsManagerCubit(), ), diff --git a/test/helpers/test_games.dart b/test/helpers/test_games.dart index 10caa768..deeba2c3 100644 --- a/test/helpers/test_games.dart +++ b/test/helpers/test_games.dart @@ -20,9 +20,7 @@ class PinballTestGame extends PinballGame { : _assets = assets, super( audio: MockPinballAudio(), - theme: const PinballTheme( - characterTheme: DashTheme(), - ), + characterTheme: const DashTheme(), ); final List? _assets; @@ -40,9 +38,7 @@ class DebugPinballTestGame extends DebugPinballGame { : _assets = assets, super( audio: MockPinballAudio(), - theme: const PinballTheme( - characterTheme: DashTheme(), - ), + characterTheme: const DashTheme(), ); final List? _assets; diff --git a/test/leaderboard/bloc/leaderboard_state_test.dart b/test/leaderboard/bloc/leaderboard_state_test.dart index ec455f1f..1b5d41d9 100644 --- a/test/leaderboard/bloc/leaderboard_state_test.dart +++ b/test/leaderboard/bloc/leaderboard_state_test.dart @@ -30,7 +30,7 @@ void main() { rank: '1', playerInitials: 'ABC', score: 1500, - character: DashTheme().character, + character: DashTheme().leaderboardIcon, ); test( diff --git a/test/leaderboard/view/leaderboard_page_test.dart b/test/leaderboard/view/leaderboard_page_test.dart index b17c27b8..daacb4a7 100644 --- a/test/leaderboard/view/leaderboard_page_test.dart +++ b/test/leaderboard/view/leaderboard_page_test.dart @@ -121,7 +121,7 @@ void main() { rank: '1', playerInitials: 'ABC', score: 10000, - character: DashTheme().character, + character: DashTheme().leaderboardIcon, ), ], ), diff --git a/test/select_character/cubit/character_theme_cubit_test.dart b/test/select_character/cubit/character_theme_cubit_test.dart new file mode 100644 index 00000000..967eb1e1 --- /dev/null +++ b/test/select_character/cubit/character_theme_cubit_test.dart @@ -0,0 +1,25 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +void main() { + group('CharacterThemeCubit', () { + test('initial state has Dash character theme', () { + final characterThemeCubit = CharacterThemeCubit(); + expect( + characterThemeCubit.state.characterTheme, + equals(const DashTheme()), + ); + }); + + blocTest( + 'charcterSelected emits selected character theme', + build: CharacterThemeCubit.new, + act: (bloc) => bloc.characterSelected(const SparkyTheme()), + expect: () => [ + const CharacterThemeState(SparkyTheme()), + ], + ); + }); +} diff --git a/test/theme/cubit/theme_state_test.dart b/test/select_character/cubit/character_theme_state_test.dart similarity index 54% rename from test/theme/cubit/theme_state_test.dart rename to test/select_character/cubit/character_theme_state_test.dart index 49a2a387..c0d584e2 100644 --- a/test/theme/cubit/theme_state_test.dart +++ b/test/select_character/cubit/character_theme_state_test.dart @@ -1,18 +1,18 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; void main() { group('ThemeState', () { test('can be instantiated', () { - expect(const ThemeState.initial(), isNotNull); + expect(const CharacterThemeState.initial(), isNotNull); }); test('supports value equality', () { expect( - ThemeState.initial(), - equals(const ThemeState.initial()), + CharacterThemeState.initial(), + equals(const CharacterThemeState.initial()), ); }); }); diff --git a/test/theme/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart similarity index 79% rename from test/theme/view/character_selection_page_test.dart rename to test/select_character/view/character_selection_page_test.dart index dcf54a13..0dda92d7 100644 --- a/test/theme/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -4,21 +4,21 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; -import 'package:pinball/theme/theme.dart'; import 'package:pinball_theme/pinball_theme.dart'; import '../../helpers/helpers.dart'; void main() { - late ThemeCubit themeCubit; + late CharacterThemeCubit characterThemeCubit; setUp(() { - themeCubit = MockThemeCubit(); + characterThemeCubit = MockCharacterThemeCubit(); whenListen( - themeCubit, - const Stream.empty(), - initialState: const ThemeState.initial(), + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), ); }); @@ -26,7 +26,7 @@ void main() { testWidgets('renders CharacterSelectionView', (tester) async { await tester.pumpApp( CharacterSelectionDialog(), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); expect(find.byType(CharacterSelectionView), findsOneWidget); }); @@ -46,7 +46,7 @@ void main() { }, ), ), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); await tester.tap(find.text('Tap me')); @@ -61,7 +61,7 @@ void main() { const titleText = 'Choose your character!'; await tester.pumpApp( CharacterSelectionView(), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); expect(find.text(titleText), findsOneWidget); @@ -75,19 +75,20 @@ void main() { await tester.pumpApp( CharacterSelectionView(), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); await tester.tap(find.byKey(sparkyButtonKey)); - verify(() => themeCubit.characterSelected(SparkyTheme())).called(1); + verify(() => characterThemeCubit.characterSelected(SparkyTheme())) + .called(1); }); testWidgets('displays how to play dialog when start is tapped', (tester) async { await tester.pumpApp( CharacterSelectionView(), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); await tester.ensureVisible(find.byType(TextButton)); await tester.tap(find.byType(TextButton)); @@ -100,7 +101,7 @@ void main() { testWidgets('CharacterImageButton renders correctly', (tester) async { await tester.pumpApp( CharacterImageButton(DashTheme()), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); expect(find.byType(Image), findsOneWidget); diff --git a/test/theme/cubit/theme_cubit_test.dart b/test/theme/cubit/theme_cubit_test.dart deleted file mode 100644 index 1f2d24e0..00000000 --- a/test/theme/cubit/theme_cubit_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/theme/theme.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -void main() { - group('ThemeCubit', () { - test('initial state has Dash character theme', () { - final themeCubit = ThemeCubit(); - expect(themeCubit.state.theme.characterTheme, equals(const DashTheme())); - }); - - blocTest( - 'charcterSelected emits selected character theme', - build: ThemeCubit.new, - act: (bloc) => bloc.characterSelected(const SparkyTheme()), - expect: () => [ - const ThemeState(PinballTheme(characterTheme: SparkyTheme())), - ], - ); - }); -} diff --git a/web/index.html b/web/index.html index ff6e451b..37e17170 100644 --- a/web/index.html +++ b/web/index.html @@ -19,18 +19,42 @@ - + + + + + + + + + + + + + + + + + + + + - + - Pinball + I/O Pinball Machine - Flutter