From 79624f07f14ddd2c14895e0c4f9f740e52a65fda Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 4 May 2022 22:13:47 +0200 Subject: [PATCH 01/10] feat: `SpaceshipRamp` shot logic (#296) * feat: spaceship ramp added cubit and behavior to sensor * refactor: changed spaceshipt ramp sensor, cubit and behavior names * refactor: added behaviors to AndroidAcres * refactor: connect rampsensors with android acres bonus * refactor: move ramp sensor to spaceship ramp children * test: fixed some tests for ramp * chore: removed unused imports * chore: removed unused import * Update lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart Co-authored-by: Alejandro Santiago * refactor: search sensors from parent children * refactor: moved ramp sensor cubit to spaceship ramp * refactor: modified ramp behaviors * refactor: fixed ramp behaviors tests * refactor: changed ramp behaviors * chore: analysis errors * test: fixed ramp contact test * test: coverage for spaceshipramp * Update packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart Co-authored-by: Alejandro Santiago * refactor: fixed test when removing children from spaceship test constructor * refactor: moved arrow state to cubit inside ramp instead of propagate from behavior * refactor: sandbox for spaceshipramp modified * refactor: removed arrow value from spaceship ramp state to sprite logic * test: golden tests for ramp arrow * test: coverage * test: coverage * refactor: changed name for RampBallAscendingContactBehavior * refactor: added ScoringBehavior on shot and bonus behaviors * feat: cancel subscription on ramp behavior remove * chore: unused import Co-authored-by: Alejandro Santiago --- .../android_acres/android_acres.dart | 11 +- .../android_acres/behaviors/behaviors.dart | 2 + .../behaviors/ramp_bonus_behavior.dart | 62 ++++++ .../behaviors/ramp_shot_behavior.dart | 63 ++++++ .../lib/src/components/components.dart | 2 +- .../spaceship_ramp/behavior/behavior.dart | 1 + .../ramp_ball_ascending_contact_behavior.dart | 24 +++ .../cubit/spaceship_ramp_cubit.dart | 16 ++ .../cubit/spaceship_ramp_state.dart | 24 +++ .../{ => spaceship_ramp}/spaceship_ramp.dart | 199 ++++++++++++------ .../android_acres/spaceship_ramp_game.dart | 2 +- ..._ball_ascending_contact_behavior_test.dart | 117 ++++++++++ .../cubit/spaceship_ramp_cubit_test.dart | 25 +++ .../cubit/spaceship_ramp_state_test.dart | 78 +++++++ .../spaceship_ramp_test.dart | 158 ++++++++++---- .../behaviors/ramp_bonus_behavior_test.dart | 152 +++++++++++++ .../behaviors/ramp_shot_behavior_test.dart | 156 ++++++++++++++ test/game/pinball_game_test.dart | 5 +- 18 files changed, 988 insertions(+), 109 deletions(-) create mode 100644 lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart create mode 100644 lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart create mode 100644 packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart create mode 100644 packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart rename packages/pinball_components/lib/src/components/{ => spaceship_ramp}/spaceship_ramp.dart (77%) create mode 100644 packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart create mode 100644 packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart create mode 100644 packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart rename packages/pinball_components/test/src/components/{ => spaceship_ramp}/spaceship_ramp_test.dart (53%) create mode 100644 test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart create mode 100644 test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index 82b71741..649ef196 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -15,7 +15,16 @@ class AndroidAcres extends Component { AndroidAcres() : super( children: [ - SpaceshipRamp(), + SpaceshipRamp( + children: [ + RampShotBehavior( + points: Points.fiveThousand, + ), + RampBonusBehavior( + points: Points.oneMillion, + ), + ], + ), SpaceshipRail(), AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidAnimatronic( diff --git a/lib/game/components/android_acres/behaviors/behaviors.dart b/lib/game/components/android_acres/behaviors/behaviors.dart index e4ac5981..91b1e132 100644 --- a/lib/game/components/android_acres/behaviors/behaviors.dart +++ b/lib/game/components/android_acres/behaviors/behaviors.dart @@ -1 +1,3 @@ export 'android_spaceship_bonus_behavior.dart'; +export 'ramp_bonus_behavior.dart'; +export 'ramp_shot_behavior.dart'; diff --git a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart new file mode 100644 index 00000000..218ad8b4 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_bonus_behavior} +/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp]. +/// {@endtemplate} +class RampBonusBehavior extends Component + with ParentIsA, HasGameRef { + /// {@macro ramp_bonus_behavior} + RampBonusBehavior({ + required Points points, + }) : _points = points, + super(); + + /// Creates a [RampBonusBehavior]. + /// + /// This can be used for testing [RampBonusBehavior] in isolation. + @visibleForTesting + RampBonusBehavior.test({ + required Points points, + required this.subscription, + }) : _points = points, + super(); + + final Points _points; + + /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. + @visibleForTesting + StreamSubscription? subscription; + + @override + void onMount() { + super.onMount(); + + subscription = subscription ?? + parent.bloc.stream.listen((state) { + final achievedOneMillionPoints = state.hits % 10 == 0; + + if (achievedOneMillionPoints) { + parent.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -60), + duration: 2, + ), + ); + } + }); + } + + @override + void onRemove() { + subscription?.cancel(); + super.onRemove(); + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart new file mode 100644 index 00000000..8a9c1a9c --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart @@ -0,0 +1,63 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_shot_behavior} +/// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. +/// {@endtemplate} +class RampShotBehavior extends Component + with ParentIsA, HasGameRef { + /// {@macro ramp_shot_behavior} + RampShotBehavior({ + required Points points, + }) : _points = points, + super(); + + /// Creates a [RampShotBehavior]. + /// + /// This can be used for testing [RampShotBehavior] in isolation. + @visibleForTesting + RampShotBehavior.test({ + required Points points, + required this.subscription, + }) : _points = points, + super(); + + final Points _points; + + /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. + @visibleForTesting + StreamSubscription? subscription; + + @override + void onMount() { + super.onMount(); + + subscription = subscription ?? + parent.bloc.stream.listen((state) { + final achievedOneMillionPoints = state.hits % 10 == 0; + + if (!achievedOneMillionPoints) { + gameRef.read().add(const MultiplierIncreased()); + + parent.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -45), + ), + ); + } + }); + } + + @override + void onRemove() { + subscription?.cancel(); + super.onRemove(); + } +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 2a3d5061..bc84fb2b 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -31,7 +31,7 @@ export 'shapes/shapes.dart'; export 'signpost/signpost.dart'; export 'slingshot.dart'; export 'spaceship_rail.dart'; -export 'spaceship_ramp.dart'; +export 'spaceship_ramp/spaceship_ramp.dart'; export 'sparky_animatronic.dart'; export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_computer.dart'; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart new file mode 100644 index 00000000..1f9b6284 --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart @@ -0,0 +1 @@ +export 'ramp_ball_ascending_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart new file mode 100644 index 00000000..2d0aad7c --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart @@ -0,0 +1,24 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_ball_ascending_contact_behavior} +/// Detects an ascending [Ball] that enters into the [SpaceshipRamp]. +/// +/// The [Ball] can hit with sensor to recognize if a [Ball] goes into or out of +/// the [SpaceshipRamp]. +/// {@endtemplate} +class RampBallAscendingContactBehavior + extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + if (other.body.linearVelocity.y < 0) { + parent.parent.bloc.onAscendingBallEntered(); + } + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart new file mode 100644 index 00000000..d27a7a2c --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart @@ -0,0 +1,16 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'spaceship_ramp_state.dart'; + +class SpaceshipRampCubit extends Cubit { + SpaceshipRampCubit() : super(const SpaceshipRampState.initial()); + + void onAscendingBallEntered() { + emit( + state.copyWith(hits: state.hits + 1), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart new file mode 100644 index 00000000..7fae894f --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart @@ -0,0 +1,24 @@ +// ignore_for_file: public_member_api_docs + +part of 'spaceship_ramp_cubit.dart'; + +class SpaceshipRampState extends Equatable { + const SpaceshipRampState({ + required this.hits, + }) : assert(hits >= 0, "Hits can't be negative"); + + const SpaceshipRampState.initial() : this(hits: 0); + + final int hits; + + SpaceshipRampState copyWith({ + int? hits, + }) { + return SpaceshipRampState( + hits: hits ?? this.hits, + ); + } + + @override + List get props => [hits]; +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart similarity index 77% rename from packages/pinball_components/lib/src/components/spaceship_ramp.dart rename to packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart index c1be0943..0b407517 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart @@ -5,16 +5,35 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'cubit/spaceship_ramp_cubit.dart'; + /// {@template spaceship_ramp} /// Ramp leading into the [AndroidSpaceship]. /// {@endtemplate} class SpaceshipRamp extends Component { /// {@macro spaceship_ramp} - SpaceshipRamp() - : super( + SpaceshipRamp({ + Iterable? children, + }) : this._( + children: children, + bloc: SpaceshipRampCubit(), + ); + + SpaceshipRamp._({ + Iterable? children, + required this.bloc, + }) : super( children: [ + // TODO(ruimiguel): refactor RampScoringSensor and + // _SpaceshipRampOpening to be in only one sensor if possible. + RampScoringSensor( + children: [ + RampBallAscendingContactBehavior(), + ], + )..initialPosition = Vector2(1.7, -20.4), _SpaceshipRampOpening( outsidePriority: ZIndexes.ballOnBoard, rotation: math.pi, @@ -34,60 +53,30 @@ class SpaceshipRamp extends Component { _SpaceshipRampForegroundRailing(), _SpaceshipRampBase()..initialPosition = Vector2(1.7, -20), _SpaceshipRampBackgroundRailingSpriteComponent(), - _SpaceshipRampArrowSpriteComponent(), + SpaceshipRampArrowSpriteComponent( + current: bloc.state.hits, + ), + ...?children, ], ); - /// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. + /// Creates a [SpaceshipRamp] without any children. /// - /// If the current state is the last one it cycles back to the initial state. - void progress() => - firstChild<_SpaceshipRampArrowSpriteComponent>()?.progress(); -} - -/// Indicates the state of the arrow on the [SpaceshipRamp]. -@visibleForTesting -enum SpaceshipRampArrowSpriteState { - /// Arrow with no dashes lit up. - inactive, - - /// Arrow with 1 light lit up. - active1, + /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation. + @visibleForTesting + SpaceshipRamp.test({ + required this.bloc, + }) : super(); - /// Arrow with 2 lights lit up. - active2, - - /// Arrow with 3 lights lit up. - active3, - - /// Arrow with 4 lights lit up. - active4, - - /// Arrow with all 5 lights lit up. - active5, -} - -extension on SpaceshipRampArrowSpriteState { - String get path { - switch (this) { - case SpaceshipRampArrowSpriteState.inactive: - return Assets.images.android.ramp.arrow.inactive.keyName; - case SpaceshipRampArrowSpriteState.active1: - return Assets.images.android.ramp.arrow.active1.keyName; - case SpaceshipRampArrowSpriteState.active2: - return Assets.images.android.ramp.arrow.active2.keyName; - case SpaceshipRampArrowSpriteState.active3: - return Assets.images.android.ramp.arrow.active3.keyName; - case SpaceshipRampArrowSpriteState.active4: - return Assets.images.android.ramp.arrow.active4.keyName; - case SpaceshipRampArrowSpriteState.active5: - return Assets.images.android.ramp.arrow.active5.keyName; - } - } + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final SpaceshipRampCubit bloc; - SpaceshipRampArrowSpriteState get next { - return SpaceshipRampArrowSpriteState - .values[(index + 1) % SpaceshipRampArrowSpriteState.values.length]; + @override + void onRemove() { + bloc.close(); + super.onRemove(); } } @@ -194,37 +183,81 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent /// /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// {@endtemplate} -class _SpaceshipRampArrowSpriteComponent - extends SpriteGroupComponent - with HasGameRef, ZIndex { +@visibleForTesting +class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent + with HasGameRef, ParentIsA, ZIndex { /// {@macro spaceship_ramp_arrow_sprite_component} - _SpaceshipRampArrowSpriteComponent() - : super( + SpaceshipRampArrowSpriteComponent({ + required int current, + }) : super( anchor: Anchor.center, position: Vector2(-3.9, -56.5), + current: current, ) { zIndex = ZIndexes.spaceshipRampArrow; } - /// Changes arrow image to the next [Sprite]. - void progress() => current = current?.next; - @override Future onLoad() async { await super.onLoad(); - final sprites = {}; + parent.bloc.stream.listen((state) { + current = state.hits % SpaceshipRampArrowSpriteState.values.length; + }); + + final sprites = {}; this.sprites = sprites; for (final spriteState in SpaceshipRampArrowSpriteState.values) { - sprites[spriteState] = Sprite( + sprites[spriteState.index] = Sprite( gameRef.images.fromCache(spriteState.path), ); } - current = SpaceshipRampArrowSpriteState.inactive; + current = 0; size = sprites[current]!.originalSize / 10; } } +/// Indicates the state of the arrow on the [SpaceshipRamp]. +@visibleForTesting +enum SpaceshipRampArrowSpriteState { + /// Arrow with no dashes lit up. + inactive, + + /// Arrow with 1 light lit up. + active1, + + /// Arrow with 2 lights lit up. + active2, + + /// Arrow with 3 lights lit up. + active3, + + /// Arrow with 4 lights lit up. + active4, + + /// Arrow with all 5 lights lit up. + active5, +} + +extension on SpaceshipRampArrowSpriteState { + String get path { + switch (this) { + case SpaceshipRampArrowSpriteState.inactive: + return Assets.images.android.ramp.arrow.inactive.keyName; + case SpaceshipRampArrowSpriteState.active1: + return Assets.images.android.ramp.arrow.active1.keyName; + case SpaceshipRampArrowSpriteState.active2: + return Assets.images.android.ramp.arrow.active2.keyName; + case SpaceshipRampArrowSpriteState.active3: + return Assets.images.android.ramp.arrow.active3.keyName; + case SpaceshipRampArrowSpriteState.active4: + return Assets.images.android.ramp.arrow.active4.keyName; + case SpaceshipRampArrowSpriteState.active5: + return Assets.images.android.ramp.arrow.active5.keyName; + } + } +} + class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent with HasGameRef, ZIndex { _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) { @@ -373,3 +406,47 @@ class _SpaceshipRampOpening extends LayerSensor { ); } } + +/// {@template ramp_scoring_sensor} +/// Small sensor body used to detect when a ball has entered the +/// [SpaceshipRamp]. +/// {@endtemplate} +class RampScoringSensor extends BodyComponent + with ParentIsA, InitialPosition, Layered { + /// {@macro ramp_scoring_sensor} + RampScoringSensor({ + Iterable? children, + }) : super( + children: children, + renderBody: false, + ) { + layer = Layer.spaceshipEntranceRamp; + } + + /// Creates a [RampScoringSensor] without any children. + /// + @visibleForTesting + RampScoringSensor.test(); + + @override + Body createBody() { + final shape = PolygonShape() + ..setAsBox( + 2.6, + .5, + initialPosition, + -5 * math.pi / 180, + ); + + 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/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart index cabe4d54..3446670a 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart @@ -54,7 +54,7 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { ) { if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.space) { - _spaceshipRamp.progress(); + _spaceshipRamp.bloc.onAscendingBallEntered(); return KeyEventResult.handled; } diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart new file mode 100644 index 00000000..ea37550a --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart @@ -0,0 +1,117 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _MockBall extends Mock implements Ball {} + +class _MockBody extends Mock implements Body {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + ]; + + final flameTester = FlameTester(() => TestGame(assets)); + + group( + 'RampBallAscendingContactBehavior', + () { + test('can be instantiated', () { + expect( + RampBallAscendingContactBehavior(), + isA(), + ); + }); + + group('beginContact', () { + late Ball ball; + late Body body; + + setUp(() { + ball = _MockBall(); + body = _MockBody(); + + when(() => ball.body).thenReturn(body); + }); + + flameTester.test( + "calls 'onAscendingBallEntered' when a ball enters into the ramp", + (game) async { + final behavior = RampBallAscendingContactBehavior(); + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SpaceshipRampState.initial(), + ); + + final rampSensor = RampScoringSensor.test(); + final spaceshipRamp = SpaceshipRamp.test( + bloc: bloc, + ); + + when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); + + await spaceshipRamp.add(rampSensor); + await game.ensureAddAll([spaceshipRamp, ball]); + await rampSensor.add(behavior); + + behavior.beginContact(ball, _MockContact()); + + verify(bloc.onAscendingBallEntered).called(1); + }, + ); + + flameTester.test( + "doesn't call 'onAscendingBallEntered' when a ball goes out the ramp", + (game) async { + final behavior = RampBallAscendingContactBehavior(); + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SpaceshipRampState.initial(), + ); + + final rampSensor = RampScoringSensor.test(); + final spaceshipRamp = SpaceshipRamp.test( + bloc: bloc, + ); + + when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); + + await spaceshipRamp.add(rampSensor); + await game.ensureAddAll([spaceshipRamp, ball]); + await rampSensor.add(behavior); + + behavior.beginContact(ball, _MockContact()); + + verifyNever(bloc.onAscendingBallEntered); + }, + ); + }); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart new file mode 100644 index 00000000..b7e899fe --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart @@ -0,0 +1,25 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('SpaceshipRampCubit', () { + group('onAscendingBallEntered', () { + blocTest( + 'emits hits incremented and arrow goes to the next value', + build: SpaceshipRampCubit.new, + act: (bloc) => bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(), + expect: () => [ + SpaceshipRampState(hits: 1), + SpaceshipRampState(hits: 2), + SpaceshipRampState(hits: 3), + ], + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart new file mode 100644 index 00000000..536f4e8e --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart @@ -0,0 +1,78 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/components/components.dart'; + +void main() { + group('SpaceshipRampState', () { + test('supports value equality', () { + expect( + SpaceshipRampState(hits: 0), + equals( + SpaceshipRampState(hits: 0), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + SpaceshipRampState(hits: 0), + isNotNull, + ); + }); + }); + + test( + 'throws AssertionError ' + 'when hits is negative', + () { + expect( + () => SpaceshipRampState(hits: -1), + throwsAssertionError, + ); + }, + ); + + group('copyWith', () { + test( + 'throws AssertionError ' + 'when hits is decreased', + () { + const rampState = SpaceshipRampState(hits: 0); + expect( + () => rampState.copyWith(hits: rampState.hits - 1), + throwsAssertionError, + ); + }, + ); + + test( + 'copies correctly ' + 'when no argument specified', + () { + const rampState = SpaceshipRampState(hits: 0); + expect( + rampState.copyWith(), + equals(rampState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + const rampState = SpaceshipRampState(hits: 0); + final otherRampState = SpaceshipRampState(hits: rampState.hits + 1); + expect(rampState, isNot(equals(otherRampState))); + + expect( + rampState.copyWith(hits: rampState.hits + 1), + equals(otherRampState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart similarity index 53% rename from packages/pinball_components/test/src/components/spaceship_ramp_test.dart rename to packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart index 0f2ce13a..b74cfb88 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart @@ -1,12 +1,16 @@ // ignore_for_file: cascade_invocations +import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -25,28 +29,35 @@ void main() { final flameTester = FlameTester(() => TestGame(assets)); group('SpaceshipRamp', () { - flameTester.test('loads correctly', (game) async { - final component = SpaceshipRamp(); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); - }); + flameTester.test( + 'loads correctly', + (game) async { + final spaceshipRamp = SpaceshipRamp(); + await game.ensureAdd(spaceshipRamp); + expect(game.children, contains(spaceshipRamp)); + }, + ); group('renders correctly', () { - const goldenFilePath = 'golden/spaceship_ramp/'; + const goldenFilePath = '../golden/spaceship_ramp/'; final centerForSpaceshipRamp = Vector2(-13, -55); flameTester.testGameWidget( 'inactive sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.inactive, ); @@ -64,15 +75,21 @@ void main() { 'active1 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component.progress(); + ramp.bloc.onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active1, ); @@ -90,17 +107,23 @@ void main() { 'active2 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active2, ); @@ -118,18 +141,24 @@ void main() { 'active3 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active3, ); @@ -147,19 +176,25 @@ void main() { 'active4 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress() - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active4, ); @@ -177,20 +212,26 @@ void main() { 'active5 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress() - ..progress() - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active5, ); @@ -204,5 +245,34 @@ void main() { }, ); }); + + flameTester.test('closes bloc when removed', (game) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final ramp = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ramp); + game.remove(ramp); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final ramp = SpaceshipRamp(children: [component]); + await game.ensureAdd(ramp); + expect(ramp.children, contains(component)); + }); + }); }); } diff --git a/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart new file mode 100644 index 00000000..acd17717 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart @@ -0,0 +1,152 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.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'; + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _MockStreamSubscription extends Mock + implements StreamSubscription {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.score.oneMillion.keyName, + ]; + + group('RampBonusBehavior', () { + const shotPoints = Points.oneMillion; + + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + flameBlocTester.testGameWidget( + 'when hits are multiples of 10 times adds a ScoringBehavior', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState(hits: 9), + ); + final behavior = RampBonusBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 10)); + + final scores = game.descendants().whereType(); + await game.ready(); + + expect(scores.length, 1); + }, + ); + + flameBlocTester.testGameWidget( + "when hits are not multiple of 10 times doesn't add any ScoringBehavior", + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState.initial(), + ); + final behavior = RampBonusBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 1)); + + final scores = game.descendants().whereType(); + await game.ready(); + + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'closes subscription when removed', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final subscription = _MockStreamSubscription(); + when(subscription.cancel).thenAnswer((_) async {}); + + final behavior = RampBonusBehavior.test( + points: shotPoints, + subscription: subscription, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + parent.remove(behavior); + await game.ready(); + + verify(subscription.cancel).called(1); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart new file mode 100644 index 00000000..23f02220 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart @@ -0,0 +1,156 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.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'; + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _MockStreamSubscription extends Mock + implements StreamSubscription {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.score.fiveThousand.keyName, + ]; + + group('RampShotBehavior', () { + const shotPoints = Points.fiveThousand; + + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + flameBlocTester.testGameWidget( + 'when hits are not multiple of 10 times ' + 'increases multiplier and adds a ScoringBehavior', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState.initial(), + ); + final behavior = RampShotBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 1)); + + final scores = game.descendants().whereType(); + await game.ready(); + + verify(() => gameBloc.add(MultiplierIncreased())).called(1); + expect(scores.length, 1); + }, + ); + + flameBlocTester.testGameWidget( + 'when hits multiple of 10 times ' + "doesn't increase multiplier, neither ScoringBehavior", + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState(hits: 9), + ); + final behavior = RampShotBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 10)); + + final scores = game.children.whereType(); + await game.ready(); + + verifyNever(() => gameBloc.add(MultiplierIncreased())); + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'closes subscription when removed', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final subscription = _MockStreamSubscription(); + when(subscription.cancel).thenAnswer((_) async {}); + + final behavior = RampShotBehavior.test( + points: shotPoints, + subscription: subscription, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + parent.remove(behavior); + await game.ready(); + + verify(subscription.cancel).called(1); + }, + ); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index ca31f280..f942c47c 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -513,8 +513,11 @@ void main() { game.onTapUp(0, tapUpEvent); await game.ready(); + final currentBalls = + game.descendants().whereType().toList(); + expect( - game.descendants().whereType().length, + currentBalls.length, equals(previousBalls.length + 1), ); }, From e3355bccbd6ac033d7f858300c5630fbe37bce7a Mon Sep 17 00:00:00 2001 From: arturplaczek <33895544+arturplaczek@users.noreply.github.com> Date: Wed, 4 May 2022 22:22:03 +0200 Subject: [PATCH 02/10] fix: update character selection dialog (#336) --- .../view/character_selection_page.dart | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index be671dd1..1f7b0374 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -65,36 +65,40 @@ class _CharacterGrid extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Column( - children: [ - _Character( - key: const Key('sparky_character_selection'), - character: const SparkyTheme(), - isSelected: state.isSparkySelected, - ), - const SizedBox(height: 6), - _Character( - key: const Key('android_character_selection'), - character: const AndroidTheme(), - isSelected: state.isAndroidSelected, - ), - ], + Expanded( + child: Column( + children: [ + _Character( + key: const Key('sparky_character_selection'), + character: const SparkyTheme(), + isSelected: state.isSparkySelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('android_character_selection'), + character: const AndroidTheme(), + isSelected: state.isAndroidSelected, + ), + ], + ), ), const SizedBox(width: 6), - Column( - children: [ - _Character( - key: const Key('dash_character_selection'), - character: const DashTheme(), - isSelected: state.isDashSelected, - ), - const SizedBox(height: 6), - _Character( - key: const Key('dino_character_selection'), - character: const DinoTheme(), - isSelected: state.isDinoSelected, - ), - ], + Expanded( + child: Column( + children: [ + _Character( + key: const Key('dash_character_selection'), + character: const DashTheme(), + isSelected: state.isDashSelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('dino_character_selection'), + character: const DinoTheme(), + isSelected: state.isDinoSelected, + ), + ], + ), ), ], ); From 86626bb059b4625a257313a57a6e8b94c3e0184e Mon Sep 17 00:00:00 2001 From: Tom Arra Date: Wed, 4 May 2022 15:41:22 -0500 Subject: [PATCH 03/10] fix: actually refrence storage rules and add cache control (#302) --- firebase.json | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/firebase.json b/firebase.json index 80e2ae69..7fbb8d30 100644 --- a/firebase.json +++ b/firebase.json @@ -2,10 +2,29 @@ "hosting": { "public": "build/web", "site": "ashehwkdkdjruejdnensjsjdne", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "headers": [ + { + "source": "**/*.@(jpg|jpeg|gif|png)", + "headers": [ + { + "key": "Cache-Control", + "value": "max-age=3600" + } + ] + }, + { + "source": "**", + "headers": [ + { + "key": "Cache-Control", + "value": "no-cache, no-store, must-revalidate" + } + ] + } ] + }, + "storage": { + "rules": "storage.rules" } } From ec6cdba0e814f046d26393ef35be15f49ef4d23a Mon Sep 17 00:00:00 2001 From: Tom Arra Date: Wed, 4 May 2022 15:51:39 -0500 Subject: [PATCH 04/10] feat: adding firestore rules (#322) * feat: adding firestore rules * Update path * making it not specific to pinball-dev --- firebase.json | 3 +++ firestore.rules | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 firestore.rules diff --git a/firebase.json b/firebase.json index 7fbb8d30..1338aeba 100644 --- a/firebase.json +++ b/firebase.json @@ -1,4 +1,7 @@ { + "firestore": { + "rules": "firestore.rules" + }, "hosting": { "public": "build/web", "site": "ashehwkdkdjruejdnensjsjdne", diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 00000000..db8d29c1 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,29 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /leaderboard/{userId} { + + function prohibited(initials) { + let prohibitedInitials = get(/databases/$(database)/documents/prohibitedInitials/list).data.prohibitedInitials; + return initials in prohibitedInitials; + } + + function inCharLimit(initials) { + return initials.size() < 4; + } + + function isAuthedUser(auth) { + return request.auth.uid != null; && auth.token.firebase.sign_in_provider == "anonymous" + } + + // Leaderboard can be read if it doesn't contain any prohibited initials + allow read: if !prohibited(resource.data.playerInitials); + + // A leaderboard entry can be created if the user is authenticated, + // it's 3 characters long, and not a prohibited combination. + allow create: if isAuthedUser(request.auth) && + inCharLimit(request.resource.data.playerInitials) && + !prohibited(request.resource.data.playerInitials); + } + } +} \ No newline at end of file From de3569c479dc43469b048cb3bde7193928c99a60 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 4 May 2022 23:04:16 +0200 Subject: [PATCH 05/10] feat: include `Ball` final assets (#279) * refactor: load ball assets from cache * feat: new ball assets * refactor: ball loads new sprites * refactor: modified ball assets for sandbox * refactor: removing baseColor from ball * refactor: moved ball assets to pinball_theme * refactor: removed all baseColor for ball * refactor: removed unused imports * test: golden tests for ball * test: added missed ball assets to tests * chore: reorder imports * refactor: removed all default ball, use dash * test: fixed test for ball changes * test: adde ball assets to forest tests * fix: fixed tests for ball * refactor: removed baseColor from ball tests * chore: unused imports * test: removed golden tests for balls * refactor: removed unused assets at test and renamed spriteAsset for ball * refactor: spriteAsset changed for ball sprite Co-authored-by: Tom Arra --- lib/game/components/controlled_ball.dart | 7 ++- lib/game/game_assets.dart | 5 +- .../assets/images/ball/ball.png | Bin 3190 -> 0 bytes .../lib/src/components/ball/ball.dart | 28 +++++++----- .../sandbox/lib/common/games.dart | 8 ++++ .../android_acres/android_bumper_a_game.dart | 1 - .../android_acres/android_bumper_b_game.dart | 1 - .../android_acres/spaceship_rail_game.dart | 2 - .../android_acres/spaceship_ramp_game.dart | 1 - .../lib/stories/ball/ball_booster_game.dart | 15 +++++- .../lib/stories/ball/basic_ball_game.dart | 22 +++++++-- .../sandbox/lib/stories/ball/stories.dart | 9 ++-- .../google_word/google_letter_game.dart | 4 -- .../stories/launch_ramp/launch_ramp_game.dart | 2 - .../lib/stories/plunger/plunger_game.dart | 2 - .../test/src/components/ball/ball_test.dart | 43 ++++++++++-------- .../ball_gravitating_behavior_test.dart | 14 ++---- .../behaviors/ball_scaling_behavior_test.dart | 20 +++----- .../ball_turbo_charging_behavior_test.dart | 17 +++---- .../chrome_dino_chomping_behavior_test.dart | 9 ++-- .../chrome_dino_spitting_behavior_test.dart | 11 +++-- .../cubit/chrome_dino_cubit_test.dart | 3 +- .../cubit/chrome_dino_state_test.dart | 3 +- .../test/src/components/flipper_test.dart | 5 +- .../src/components/golden/ball/android.png | Bin 0 -> 28038 bytes .../test/src/components/golden/ball/dash.png | Bin 0 -> 28052 bytes .../test/src/components/golden/ball/dino.png | Bin 0 -> 28658 bytes .../src/components/golden/ball/sparky.png | Bin 0 -> 27904 bytes .../assets/images/android/ball.png | Bin 0 -> 6544 bytes .../pinball_theme/assets/images/dash/ball.png | Bin 0 -> 6561 bytes .../pinball_theme/assets/images/dino/ball.png | Bin 0 -> 6973 bytes .../assets/images/sparky/ball.png | Bin 0 -> 6449 bytes .../lib/src/generated/assets.gen.dart | 22 ++++----- .../lib/src/themes/android_theme.dart | 3 +- .../lib/src/themes/character_theme.dart | 7 ++- .../lib/src/themes/dash_theme.dart | 3 +- .../lib/src/themes/dino_theme.dart | 3 +- .../lib/src/themes/sparky_theme.dart | 3 +- .../game/components/controlled_ball_test.dart | 12 +++-- .../flutter_forest_bonus_behavior_test.dart | 9 +++- test/game/pinball_game_test.dart | 6 ++- 41 files changed, 167 insertions(+), 133 deletions(-) delete mode 100644 packages/pinball_components/assets/images/ball/ball.png create mode 100644 packages/pinball_components/test/src/components/golden/ball/android.png create mode 100644 packages/pinball_components/test/src/components/golden/ball/dash.png create mode 100644 packages/pinball_components/test/src/components/golden/ball/dino.png create mode 100644 packages/pinball_components/test/src/components/golden/ball/sparky.png create mode 100644 packages/pinball_theme/assets/images/android/ball.png create mode 100644 packages/pinball_theme/assets/images/dash/ball.png create mode 100644 packages/pinball_theme/assets/images/dino/ball.png create mode 100644 packages/pinball_theme/assets/images/sparky/ball.png diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 9dc81135..132639d4 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,7 +1,6 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.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'; @@ -17,7 +16,7 @@ class ControlledBall extends Ball with Controls { /// A [Ball] that launches from the [Plunger]. ControlledBall.launch({ required CharacterTheme characterTheme, - }) : super(baseColor: characterTheme.ballColor) { + }) : super(assetPath: characterTheme.ball.keyName) { controller = BallController(this); layer = Layer.launcher; zIndex = ZIndexes.ballOnLaunchRamp; @@ -28,13 +27,13 @@ class ControlledBall extends Ball with Controls { /// {@endtemplate} ControlledBall.bonus({ required CharacterTheme characterTheme, - }) : super(baseColor: characterTheme.ballColor) { + }) : super(assetPath: characterTheme.ball.keyName) { controller = BallController(this); zIndex = ZIndexes.ballOnBoard; } /// [Ball] used in [DebugPinballGame]. - ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { + ControlledBall.debug() : super() { controller = BallController(this); zIndex = ZIndexes.ballOnBoard; } diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 7ab86553..d066ce0d 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -13,7 +13,6 @@ extension PinballGameAssetsX on PinballGame { return [ images.load(components.Assets.images.boardBackground.keyName), - images.load(components.Assets.images.ball.ball.keyName), images.load(components.Assets.images.ball.flameEffect.keyName), images.load(components.Assets.images.signpost.inactive.keyName), images.load(components.Assets.images.signpost.active1.keyName), @@ -136,6 +135,10 @@ extension PinballGameAssetsX on PinballGame { images.load(sparkyTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName), images.load(dinoTheme.leaderboardIcon.keyName), + images.load(androidTheme.ball.keyName), + images.load(dashTheme.ball.keyName), + images.load(dinoTheme.ball.keyName), + images.load(sparkyTheme.ball.keyName), ]; } } diff --git a/packages/pinball_components/assets/images/ball/ball.png b/packages/pinball_components/assets/images/ball/ball.png deleted file mode 100644 index 43332c9aba015855ea7d77252cd9b3c6ca339e38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3190 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 Q;70=FiS>1_#f0Vj56Hwch5!Hn diff --git a/packages/pinball_components/lib/src/components/ball/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart index 9234e69c..e8cea997 100644 --- a/packages/pinball_components/lib/src/components/ball/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -2,9 +2,10 @@ import 'dart:async'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; export 'behaviors/behaviors.dart'; @@ -14,11 +15,11 @@ export 'behaviors/behaviors.dart'; class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// {@macro ball} Ball({ - required this.baseColor, + String? assetPath, }) : super( renderBody: false, children: [ - _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), + _BallSpriteComponent(assetPath: assetPath), BallScalingBehavior(), BallGravitatingBehavior(), ], @@ -35,7 +36,7 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// /// This can be used for testing [Ball]'s behaviors in isolation. @visibleForTesting - Ball.test({required this.baseColor}) + Ball.test() : super( children: [_BallSpriteComponent()], ); @@ -43,9 +44,6 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// The size of the [Ball]. static final Vector2 size = Vector2.all(4.13); - /// The base [Color] used to tint this [Ball]. - final Color baseColor; - @override Body createBody() { final shape = CircleShape()..radius = size.x / 2; @@ -79,14 +77,22 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { } class _BallSpriteComponent extends SpriteComponent with HasGameRef { + _BallSpriteComponent({ + this.assetPath, + }) : super( + anchor: Anchor.center, + ); + + final String? assetPath; + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.ball.ball.keyName, + final sprite = Sprite( + gameRef.images + .fromCache(assetPath ?? theme.Assets.images.dash.ball.keyName), ); this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.center; + size = sprite.originalSize / 12.5; } } diff --git a/packages/pinball_components/sandbox/lib/common/games.dart b/packages/pinball_components/sandbox/lib/common/games.dart index 89d16450..bee6a280 100644 --- a/packages/pinball_components/sandbox/lib/common/games.dart +++ b/packages/pinball_components/sandbox/lib/common/games.dart @@ -24,6 +24,14 @@ abstract class AssetsGame extends Forge2DGame { } abstract class LineGame extends AssetsGame with PanDetector { + LineGame({ + List? imagesFileNames, + }) : super( + imagesFileNames: [ + if (imagesFileNames != null) ...imagesFileNames, + ], + ); + Vector2? _lineEnd; @override diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart index 32638c2d..78cebd95 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart @@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; class AndroidBumperAGame extends BallGame { AndroidBumperAGame() : super( - color: const Color(0xFF0000FF), imagesFileNames: [ Assets.images.android.bumper.a.lit.keyName, Assets.images.android.bumper.a.dimmed.keyName, diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart index bfd4206c..9bd2caff 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart @@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; class AndroidBumperBGame extends BallGame { AndroidBumperBGame() : super( - color: const Color(0xFF0000FF), imagesFileNames: [ Assets.images.android.bumper.b.lit.keyName, Assets.images.android.bumper.b.dimmed.keyName, diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart index dee83e26..4093ad33 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart @@ -1,14 +1,12 @@ import 'dart:async'; import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRailGame extends BallGame { SpaceshipRailGame() : super( - color: Colors.blue, ballPriority: ZIndexes.ballOnSpaceshipRail, ballLayer: Layer.spaceshipExitRail, imagesFileNames: [ diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart index 3446670a..fe4e6dae 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart @@ -9,7 +9,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRampGame extends BallGame with KeyboardEvents { SpaceshipRampGame() : super( - color: Colors.blue, ballPriority: ZIndexes.ballOnSpaceshipRamp, ballLayer: Layer.spaceshipEntranceRamp, imagesFileNames: [ 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 a66459a6..ac0989e2 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 @@ -1,9 +1,20 @@ import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:sandbox/common/common.dart'; class BallBoosterGame extends LineGame { + BallBoosterGame() + : super( + imagesFileNames: [ + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, + Assets.images.ball.flameEffect.keyName, + ], + ); + static const description = ''' Shows how a Ball with a boost works. @@ -12,7 +23,7 @@ class BallBoosterGame extends LineGame { @override void onLine(Vector2 line) { - final ball = Ball(baseColor: Colors.transparent); + final ball = Ball(); final impulse = line * -1 * 20; ball.add(BallTurboChargingBehavior(impulse: impulse)); 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 e57a0322..f3ba50f3 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 @@ -1,17 +1,20 @@ import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:sandbox/common/common.dart'; class BallGame extends AssetsGame with TapDetector, Traceable { BallGame({ - this.color = Colors.blue, this.ballPriority = 0, this.ballLayer = Layer.all, + this.character, List? imagesFileNames, }) : super( imagesFileNames: [ - Assets.images.ball.ball.keyName, + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, if (imagesFileNames != null) ...imagesFileNames, ], ); @@ -22,14 +25,23 @@ class BallGame extends AssetsGame with TapDetector, Traceable { - Tap anywhere on the screen to spawn a ball into the game. '''; - final Color color; + static final characterBallPaths = { + 'Dash': theme.Assets.images.dash.ball.keyName, + 'Sparky': theme.Assets.images.sparky.ball.keyName, + 'Android': theme.Assets.images.android.ball.keyName, + 'Dino': theme.Assets.images.dino.ball.keyName, + }; + final int ballPriority; final Layer ballLayer; + final String? character; @override void onTapUp(TapUpInfo info) { add( - Ball(baseColor: color) + Ball( + assetPath: characterBallPaths[character], + ) ..initialPosition = info.eventPosition.game ..layer = ballLayer ..priority = ballPriority, diff --git a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart index eb472282..146ebcda 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:flutter/material.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/ball_booster_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; @@ -7,10 +6,14 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; void addBallStories(Dashbook dashbook) { dashbook.storiesOf('Ball') ..addGame( - title: 'Colored', + title: 'Themed', description: BallGame.description, gameBuilder: (context) => BallGame( - color: context.colorProperty('color', Colors.blue), + character: context.listProperty( + 'Character', + BallGame.characterBallPaths.keys.first, + BallGame.characterBallPaths.keys.toList(), + ), ), ) ..addGame( 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 bc537de2..94389f60 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 @@ -1,14 +1,10 @@ -import 'dart:ui'; - import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class GoogleLetterGame extends BallGame { GoogleLetterGame() : super( - color: const Color(0xFF009900), imagesFileNames: [ Assets.images.googleWord.letter1.lit.keyName, Assets.images.googleWord.letter1.dimmed.keyName, 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 ea3bd4db..b6955a26 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 @@ -1,14 +1,12 @@ import 'dart:async'; import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class LaunchRampGame extends BallGame { LaunchRampGame() : super( - color: Colors.blue, ballPriority: ZIndexes.ballOnLaunchRamp, ballLayer: Layer.launcher, ); 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 0f1ec2e4..0ee58cc9 100644 --- a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -6,8 +6,6 @@ import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class PlungerGame extends BallGame with KeyboardEvents, Traceable { - PlungerGame() : super(color: const Color(0xFFFF0000)); - static const description = ''' Shows how Plunger is rendered. diff --git a/packages/pinball_components/test/src/components/ball/ball_test.dart b/packages/pinball_components/test/src/components/ball/ball_test.dart index 655836a0..9195e0b2 100644 --- a/packages/pinball_components/test/src/components/ball/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball/ball_test.dart @@ -2,31 +2,36 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, + ]; - group('Ball', () { - const baseColor = Color(0xFFFFFFFF); + final flameTester = FlameTester(() => TestGame(assets)); + group('Ball', () { test( 'can be instantiated', () { - expect(Ball(baseColor: baseColor), isA()); - expect(Ball.test(baseColor: baseColor), isA()); + expect(Ball(), isA()); + expect(Ball.test(), isA()); }, ); flameTester.test( 'loads correctly', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ready(); await game.ensureAdd(ball); @@ -36,7 +41,7 @@ void main() { group('adds', () { flameTester.test('a BallScalingBehavior', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect( ball.descendants().whereType().length, @@ -45,7 +50,7 @@ void main() { }); flameTester.test('a BallGravitatingBehavior', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect( ball.descendants().whereType().length, @@ -58,7 +63,7 @@ void main() { flameTester.test( 'is dynamic', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect(ball.body.bodyType, equals(BodyType.dynamic)); @@ -67,7 +72,7 @@ void main() { group('can be moved', () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); game.update(1); @@ -75,7 +80,7 @@ void main() { }); flameTester.test('by applying velocity', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.body.gravityScale = Vector2.zero(); @@ -90,7 +95,7 @@ void main() { flameTester.test( 'exists', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect(ball.body.fixtures[0], isA()); @@ -100,7 +105,7 @@ void main() { flameTester.test( 'is dense', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -111,7 +116,7 @@ void main() { flameTester.test( 'shape is circular', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -123,7 +128,7 @@ void main() { flameTester.test( 'has Layer.all as default filter maskBits', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ready(); await game.ensureAdd(ball); await game.ready(); @@ -137,7 +142,7 @@ void main() { group('stop', () { group("can't be moved", () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.stop(); @@ -152,7 +157,7 @@ void main() { flameTester.test( 'by its weight when previously stopped', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.stop(); ball.resume(); @@ -165,7 +170,7 @@ void main() { flameTester.test( 'by applying velocity when previously stopped', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.stop(); ball.resume(); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart index d78df37a..ce193dc8 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart @@ -1,21 +1,19 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final asset = Assets.images.ball.ball.keyName; + final asset = theme.Assets.images.dash.ball.keyName; final flameTester = FlameTester(() => TestGame([asset])); group('BallGravitatingBehavior', () { - const baseColor = Color(0xFFFFFFFF); test('can be instantiated', () { expect( BallGravitatingBehavior(), @@ -24,7 +22,7 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); final behavior = BallGravitatingBehavior(); await ball.add(behavior); await game.ensureAdd(ball); @@ -37,12 +35,10 @@ void main() { flameTester.test( "overrides the body's horizontal gravity symmetrically", (game) async { - final ball1 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(10, 0); + final ball1 = Ball.test()..initialPosition = Vector2(10, 0); await ball1.add(BallGravitatingBehavior()); - final ball2 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(-10, 0); + final ball2 = Ball.test()..initialPosition = Vector2(-10, 0); await ball2.add(BallGravitatingBehavior()); await game.ensureAddAll([ball1, ball2]); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart index 0aeeda98..bd0cca49 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -1,21 +1,19 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final asset = Assets.images.ball.ball.keyName; + final asset = theme.Assets.images.dash.ball.keyName; final flameTester = FlameTester(() => TestGame([asset])); group('BallScalingBehavior', () { - const baseColor = Color(0xFFFFFFFF); test('can be instantiated', () { expect( BallScalingBehavior(), @@ -24,7 +22,7 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); final behavior = BallScalingBehavior(); await ball.add(behavior); await game.ensureAdd(ball); @@ -35,12 +33,10 @@ void main() { }); flameTester.test('scales the shape radius', (game) async { - final ball1 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, 10); + final ball1 = Ball.test()..initialPosition = Vector2(0, 10); await ball1.add(BallScalingBehavior()); - final ball2 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, -10); + final ball2 = Ball.test()..initialPosition = Vector2(0, -10); await ball2.add(BallScalingBehavior()); await game.ensureAddAll([ball1, ball2]); @@ -57,12 +53,10 @@ void main() { flameTester.test( 'scales the sprite', (game) async { - final ball1 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, 10); + final ball1 = Ball.test()..initialPosition = Vector2(0, 10); await ball1.add(BallScalingBehavior()); - final ball2 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, -10); + final ball2 = Ball.test()..initialPosition = Vector2(0, -10); await ball2.add(BallScalingBehavior()); await game.ensureAddAll([ball1, ball2]); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart index 00f34832..79eb030e 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart @@ -1,12 +1,10 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; @@ -16,9 +14,8 @@ void main() { group( 'BallTurboChargingBehavior', () { - final assets = [Assets.images.ball.ball.keyName]; - final flameTester = FlameTester(() => TestGame(assets)); - const baseColor = Color(0xFFFFFFFF); + final asset = theme.Assets.images.dash.ball.keyName; + final flameTester = FlameTester(() => TestGame([asset])); test('can be instantiated', () { expect( @@ -28,7 +25,7 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); await ball.add(behavior); await game.ensureAdd(ball); @@ -41,7 +38,7 @@ void main() { flameTester.test( 'impulses the ball velocity when loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); await game.ensureAdd(ball); final impulse = Vector2.all(1); final behavior = BallTurboChargingBehavior(impulse: impulse); @@ -59,7 +56,7 @@ void main() { ); flameTester.test('adds sprite', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); await ball.ensureAdd( @@ -73,7 +70,7 @@ void main() { }); flameTester.test('removes sprite after it finishes', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart index 8d052fab..dfc33967 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart @@ -4,11 +4,11 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; @@ -20,7 +20,10 @@ class _MockFixture extends Mock implements Fixture {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + theme.Assets.images.dash.ball.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group( 'ChromeDinoChompingBehavior', @@ -35,7 +38,7 @@ void main() { flameTester.test( 'beginContact sets ball sprite to be invisible and calls onChomp', (game) async { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); final behavior = ChromeDinoChompingBehavior(); final bloc = _MockChromeDinoCubit(); whenListen( diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart index 1d0a55b4..8c2cbe57 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart @@ -5,11 +5,11 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; @@ -17,7 +17,10 @@ class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + theme.Assets.images.dash.ball.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group( 'ChromeDinoSpittingBehavior', @@ -33,7 +36,7 @@ void main() { flameTester.test( 'sets ball sprite to visible and sets a linear velocity', (game) async { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); final behavior = ChromeDinoSpittingBehavior(); final bloc = _MockChromeDinoCubit(); final streamController = StreamController(); @@ -71,7 +74,7 @@ void main() { flameTester.test( 'calls onSpit', (game) async { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); final behavior = ChromeDinoSpittingBehavior(); final bloc = _MockChromeDinoCubit(); final streamController = StreamController(); diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart index 5b31be74..79375a6e 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart @@ -1,5 +1,4 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -7,7 +6,7 @@ void main() { group( 'ChromeDinoCubit', () { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); blocTest( 'onOpenMouth emits true', diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart index d067674b..442d544b 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: prefer_const_constructors -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -61,7 +60,7 @@ void main() { 'copies correctly ' 'when all arguments specified', () { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); const chromeDinoState = ChromeDinoState( status: ChromeDinoStatus.chomping, isMouthOpen: true, diff --git a/packages/pinball_components/test/src/components/flipper_test.dart b/packages/pinball_components/test/src/components/flipper_test.dart index c34d0d1c..314b1f77 100644 --- a/packages/pinball_components/test/src/components/flipper_test.dart +++ b/packages/pinball_components/test/src/components/flipper_test.dart @@ -2,9 +2,9 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../helpers/helpers.dart'; @@ -13,6 +13,7 @@ void main() { final assets = [ Assets.images.flipper.left.keyName, Assets.images.flipper.right.keyName, + theme.Assets.images.dash.ball.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); @@ -89,7 +90,7 @@ void main() { 'has greater mass than Ball', (game) async { final flipper = Flipper(side: BoardSide.left); - final ball = Ball(baseColor: Colors.white); + final ball = Ball(); await game.ready(); await game.ensureAddAll([flipper, ball]); diff --git a/packages/pinball_components/test/src/components/golden/ball/android.png b/packages/pinball_components/test/src/components/golden/ball/android.png new file mode 100644 index 0000000000000000000000000000000000000000..2a659092cba7b6b6798a7b526261ac4fac8bbfce GIT binary patch literal 28038 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 U03({atrP)X*Sl79_4d#I1@L7-YybcN literal 0 HcmV?d00001 diff --git a/packages/pinball_components/test/src/components/golden/ball/dash.png b/packages/pinball_components/test/src/components/golden/ball/dash.png new file mode 100644 index 0000000000000000000000000000000000000000..c95afc8801dee57340120032ea2c376448d4792f GIT binary patch literal 28052 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/test/src/components/golden/ball/dino.png b/packages/pinball_components/test/src/components/golden/ball/dino.png new file mode 100644 index 0000000000000000000000000000000000000000..8ea1075884ea25ca26264512a1083de45ce1cde6 GIT binary patch literal 28658 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/test/src/components/golden/ball/sparky.png b/packages/pinball_components/test/src/components/golden/ball/sparky.png new file mode 100644 index 0000000000000000000000000000000000000000..afdeb2636132d754c04370e5389fb090fed9298b GIT binary patch literal 27904 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 DFn_Y_ literal 0 HcmV?d00001 diff --git a/packages/pinball_theme/assets/images/android/ball.png b/packages/pinball_theme/assets/images/android/ball.png new file mode 100644 index 0000000000000000000000000000000000000000..b5cfbc3ff3df86e10a37b2126d7a67a3f0c8fb96 GIT binary patch literal 6544 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_theme/assets/images/dash/ball.png b/packages/pinball_theme/assets/images/dash/ball.png new file mode 100644 index 0000000000000000000000000000000000000000..fa754cbc158e12e489a783def24967f9777564d9 GIT binary patch literal 6561 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_theme/assets/images/dino/ball.png b/packages/pinball_theme/assets/images/dino/ball.png new file mode 100644 index 0000000000000000000000000000000000000000..02b99c4314e78c4ff3cd2053ebfe698e65134f0d GIT binary patch literal 6973 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_theme/assets/images/sparky/ball.png b/packages/pinball_theme/assets/images/sparky/ball.png new file mode 100644 index 0000000000000000000000000000000000000000..95e5a10b6a81f879289a985640f155ee97a4ecc0 GIT binary patch literal 6449 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 LXlYQT=Mnc`O=SU) literal 0 HcmV?d00001 diff --git a/packages/pinball_theme/lib/src/generated/assets.gen.dart b/packages/pinball_theme/lib/src/generated/assets.gen.dart index 3feeecce..545f514b 100644 --- a/packages/pinball_theme/lib/src/generated/assets.gen.dart +++ b/packages/pinball_theme/lib/src/generated/assets.gen.dart @@ -36,9 +36,9 @@ class $AssetsImagesAndroidGen { AssetGenImage get background => const AssetGenImage('assets/images/android/background.png'); - /// File path: assets/images/android/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/android/character.png'); + /// File path: assets/images/android/ball.png + AssetGenImage get ball => + const AssetGenImage('assets/images/android/ball.png'); /// File path: assets/images/android/icon.png AssetGenImage get icon => @@ -60,9 +60,8 @@ class $AssetsImagesDashGen { AssetGenImage get background => const AssetGenImage('assets/images/dash/background.png'); - /// File path: assets/images/dash/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/dash/character.png'); + /// File path: assets/images/dash/ball.png + AssetGenImage get ball => const AssetGenImage('assets/images/dash/ball.png'); /// File path: assets/images/dash/icon.png AssetGenImage get icon => const AssetGenImage('assets/images/dash/icon.png'); @@ -83,9 +82,8 @@ class $AssetsImagesDinoGen { AssetGenImage get background => const AssetGenImage('assets/images/dino/background.png'); - /// File path: assets/images/dino/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/dino/character.png'); + /// File path: assets/images/dino/ball.png + AssetGenImage get ball => const AssetGenImage('assets/images/dino/ball.png'); /// File path: assets/images/dino/icon.png AssetGenImage get icon => const AssetGenImage('assets/images/dino/icon.png'); @@ -106,9 +104,9 @@ class $AssetsImagesSparkyGen { AssetGenImage get background => const AssetGenImage('assets/images/sparky/background.png'); - /// File path: assets/images/sparky/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/sparky/character.png'); + /// File path: assets/images/sparky/ball.png + AssetGenImage get ball => + const AssetGenImage('assets/images/sparky/ball.png'); /// File path: assets/images/sparky/icon.png AssetGenImage get icon => diff --git a/packages/pinball_theme/lib/src/themes/android_theme.dart b/packages/pinball_theme/lib/src/themes/android_theme.dart index 8989c717..6e7d76b2 100644 --- a/packages/pinball_theme/lib/src/themes/android_theme.dart +++ b/packages/pinball_theme/lib/src/themes/android_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template android_theme} @@ -12,7 +11,7 @@ class AndroidTheme extends CharacterTheme { String get name => 'Android'; @override - Color get ballColor => Colors.green; + AssetGenImage get ball => Assets.images.android.ball; @override AssetGenImage get background => Assets.images.android.background; diff --git a/packages/pinball_theme/lib/src/themes/character_theme.dart b/packages/pinball_theme/lib/src/themes/character_theme.dart index 072c917f..596f41a0 100644 --- a/packages/pinball_theme/lib/src/themes/character_theme.dart +++ b/packages/pinball_theme/lib/src/themes/character_theme.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template character_theme} @@ -15,8 +14,8 @@ abstract class CharacterTheme extends Equatable { /// Name of character. String get name; - /// Ball color for this theme. - Color get ballColor; + /// Asset for the ball. + AssetGenImage get ball; /// Asset for the background. AssetGenImage get background; @@ -33,7 +32,7 @@ abstract class CharacterTheme extends Equatable { @override List get props => [ name, - ballColor, + ball, background, icon, leaderboardIcon, diff --git a/packages/pinball_theme/lib/src/themes/dash_theme.dart b/packages/pinball_theme/lib/src/themes/dash_theme.dart index 7584c8ed..be3a8873 100644 --- a/packages/pinball_theme/lib/src/themes/dash_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dash_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dash_theme} @@ -12,7 +11,7 @@ class DashTheme extends CharacterTheme { String get name => 'Dash'; @override - Color get ballColor => Colors.blue; + AssetGenImage get ball => Assets.images.dash.ball; @override AssetGenImage get background => Assets.images.dash.background; diff --git a/packages/pinball_theme/lib/src/themes/dino_theme.dart b/packages/pinball_theme/lib/src/themes/dino_theme.dart index 3baf466c..1de42d41 100644 --- a/packages/pinball_theme/lib/src/themes/dino_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dino_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dino_theme} @@ -12,7 +11,7 @@ class DinoTheme extends CharacterTheme { String get name => 'Dino'; @override - Color get ballColor => Colors.grey; + AssetGenImage get ball => Assets.images.dino.ball; @override AssetGenImage get background => Assets.images.dino.background; diff --git a/packages/pinball_theme/lib/src/themes/sparky_theme.dart b/packages/pinball_theme/lib/src/themes/sparky_theme.dart index 7884a22f..1699f3ae 100644 --- a/packages/pinball_theme/lib/src/themes/sparky_theme.dart +++ b/packages/pinball_theme/lib/src/themes/sparky_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template sparky_theme} @@ -9,7 +8,7 @@ class SparkyTheme extends CharacterTheme { const SparkyTheme(); @override - Color get ballColor => Colors.orange; + AssetGenImage get ball => Assets.images.sparky.ball; @override String get name => 'Sparky'; diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index d8d31b4e..dc142ffd 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -2,13 +2,12 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; -import 'package:flame/extensions.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; - import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../helpers/helpers.dart'; @@ -33,13 +32,16 @@ class _MockBall extends Mock implements Ball {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + theme.Assets.images.dash.ball.keyName, + ]; group('BallController', () { late Ball ball; late GameBloc gameBloc; setUp(() { - ball = Ball(baseColor: const Color(0xFF00FFFF)); + ball = Ball(); gameBloc = _MockGameBloc(); whenListen( gameBloc, @@ -51,6 +53,7 @@ void main() { final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, + assets: assets, ); test('can be instantiated', () { @@ -68,7 +71,7 @@ void main() { await ball.add(controller); await game.ensureAdd(ball); - final otherBall = Ball(baseColor: const Color(0xFF00FFFF)); + final otherBall = Ball(); final otherController = BallController(otherBall); await otherBall.add(otherController); await game.ensureAdd(otherBall); @@ -106,6 +109,7 @@ void main() { flameBlocTester.testGameWidget( 'adds TurboChargeActivated', setUp: (game, tester) async { + await game.images.loadAll(assets); final controller = BallController(ball); await ball.add(controller); await game.ensureAdd(ball); diff --git a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart index f9e2988d..71b41029 100644 --- a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart +++ b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart @@ -10,15 +10,21 @@ import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart' import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; class _MockGameBloc extends Mock implements GameBloc {} void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('FlutterForestBonusBehavior', () { late GameBloc gameBloc; - final assets = [Assets.images.dash.animatronic.keyName]; + final assets = [ + Assets.images.dash.animatronic.keyName, + theme.Assets.images.dash.ball.keyName, + ]; setUp(() { gameBloc = _MockGameBloc(); @@ -32,6 +38,7 @@ void main() { final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, + assets: assets, ); void _contactedBumper(DashNestBumper bumper) => diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index f942c47c..b75b3147 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_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_theme/pinball_theme.dart' as theme; import '../helpers/helpers.dart'; @@ -43,7 +44,10 @@ void main() { Assets.images.backbox.marquee.keyName, Assets.images.backbox.displayDivider.keyName, Assets.images.boardBackground.keyName, - Assets.images.ball.ball.keyName, + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, Assets.images.ball.flameEffect.keyName, Assets.images.baseboard.left.keyName, Assets.images.baseboard.right.keyName, From faba9ed85b32939938825103febda094e913ded1 Mon Sep 17 00:00:00 2001 From: Tom Arra Date: Wed, 4 May 2022 16:56:35 -0500 Subject: [PATCH 06/10] fix: extra semicolon removed (#338) --- firestore.rules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firestore.rules b/firestore.rules index db8d29c1..fbff78f0 100644 --- a/firestore.rules +++ b/firestore.rules @@ -13,7 +13,7 @@ service cloud.firestore { } function isAuthedUser(auth) { - return request.auth.uid != null; && auth.token.firebase.sign_in_provider == "anonymous" + return request.auth.uid != null && auth.token.firebase.sign_in_provider == "anonymous" } // Leaderboard can be read if it doesn't contain any prohibited initials From f6719c93cf13418cc4be6b561ecc3d19ca58b597 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Wed, 4 May 2022 17:33:37 -0500 Subject: [PATCH 07/10] fix: barrier obstructing dino movement (#339) --- lib/game/components/dino_desert/dino_desert.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/game/components/dino_desert/dino_desert.dart b/lib/game/components/dino_desert/dino_desert.dart index 9ba9c71b..5f01979f 100644 --- a/lib/game/components/dino_desert/dino_desert.dart +++ b/lib/game/components/dino_desert/dino_desert.dart @@ -36,12 +36,14 @@ class DinoDesert extends Component { } class _BarrierBehindDino extends BodyComponent { + _BarrierBehindDino() : super(renderBody: false); + @override Body createBody() { final shape = EdgeShape() ..set( - Vector2(25, -14.2), - Vector2(25, -7.7), + Vector2(25.3, -14.2), + Vector2(25.3, -7.7), ); return world.createBody(BodyDef())..createFixtureFromShape(shape); From 0595e82649ca937e6a3e144b5d94522911418123 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 5 May 2022 01:54:03 +0100 Subject: [PATCH 08/10] refactor: moving layering to `LayerFilteringBehavior` (#340) --- .../lib/src/components/components.dart | 2 +- .../lib/src/components/layer_sensor.dart | 90 ------------ .../layer_sensor/behaviors/behaviors.dart | 2 + .../behaviors/layer_filtering_behavior.dart | 31 ++++ .../components/layer_sensor/layer_sensor.dart | 66 +++++++++ .../layer_filtering_behavior_test.dart | 136 ++++++++++++++++++ .../{ => layer_sensor}/layer_sensor_test.dart | 84 +++-------- 7 files changed, 252 insertions(+), 159 deletions(-) delete mode 100644 packages/pinball_components/lib/src/components/layer_sensor.dart create mode 100644 packages/pinball_components/lib/src/components/layer_sensor/behaviors/behaviors.dart create mode 100644 packages/pinball_components/lib/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/layer_sensor/layer_sensor.dart create mode 100644 packages/pinball_components/test/src/components/layer_sensor/behavior/layer_filtering_behavior_test.dart rename packages/pinball_components/test/src/components/{ => layer_sensor}/layer_sensor_test.dart (59%) diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index bc84fb2b..5eef3538 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -21,7 +21,7 @@ export 'joint_anchor.dart'; export 'kicker/kicker.dart'; export 'launch_ramp.dart'; export 'layer.dart'; -export 'layer_sensor.dart'; +export 'layer_sensor/layer_sensor.dart'; export 'multiball/multiball.dart'; export 'multiplier/multiplier.dart'; export 'plunger.dart'; diff --git a/packages/pinball_components/lib/src/components/layer_sensor.dart b/packages/pinball_components/lib/src/components/layer_sensor.dart deleted file mode 100644 index 6b5f3832..00000000 --- a/packages/pinball_components/lib/src/components/layer_sensor.dart +++ /dev/null @@ -1,90 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template layer_entrance_orientation} -/// Determines if a layer entrance is oriented [up] or [down] on the board. -/// {@endtemplate} -enum LayerEntranceOrientation { - /// Facing up on the Board. - up, - - /// Facing down on the Board. - down, -} - -/// {@template layer_sensor} -/// [BodyComponent] located at the entrance and exit of a [Layer]. -/// -/// By default the base [layer] is set to [Layer.board] and the -/// [_outsideZIndex] is set to [ZIndexes.ballOnBoard]. -/// {@endtemplate} -abstract class LayerSensor extends BodyComponent - with InitialPosition, Layered, ContactCallbacks { - /// {@macro layer_sensor} - LayerSensor({ - required Layer insideLayer, - Layer? outsideLayer, - required int insideZIndex, - int? outsideZIndex, - required this.orientation, - }) : _insideLayer = insideLayer, - _outsideLayer = outsideLayer ?? Layer.board, - _insideZIndex = insideZIndex, - _outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard, - super(renderBody: false) { - layer = Layer.opening; - } - - final Layer _insideLayer; - final Layer _outsideLayer; - final int _insideZIndex; - final int _outsideZIndex; - - /// The [Shape] of the [LayerSensor]. - Shape get shape; - - /// {@macro layer_entrance_orientation} - // TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for - // collision calculations. - final LayerEntranceOrientation orientation; - - @override - Body createBody() { - final fixtureDef = FixtureDef( - shape, - isSensor: true, - ); - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is! Ball) return; - - if (other.layer != _insideLayer) { - final isBallEnteringOpening = - (orientation == LayerEntranceOrientation.down && - other.body.linearVelocity.y < 0) || - (orientation == LayerEntranceOrientation.up && - other.body.linearVelocity.y > 0); - - if (isBallEnteringOpening) { - other - ..layer = _insideLayer - ..zIndex = _insideZIndex; - } - } else { - other - ..layer = _outsideLayer - ..zIndex = _outsideZIndex; - } - } -} diff --git a/packages/pinball_components/lib/src/components/layer_sensor/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/layer_sensor/behaviors/behaviors.dart new file mode 100644 index 00000000..060e313d --- /dev/null +++ b/packages/pinball_components/lib/src/components/layer_sensor/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'behaviors.dart'; +export 'layer_filtering_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart b/packages/pinball_components/lib/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart new file mode 100644 index 00000000..06dca4b6 --- /dev/null +++ b/packages/pinball_components/lib/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart @@ -0,0 +1,31 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class LayerFilteringBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + if (other.layer != parent.insideLayer) { + final isBallEnteringOpening = + (parent.orientation == LayerEntranceOrientation.down && + other.body.linearVelocity.y < 0) || + (parent.orientation == LayerEntranceOrientation.up && + other.body.linearVelocity.y > 0); + + if (isBallEnteringOpening) { + other + ..layer = parent.insideLayer + ..zIndex = parent.insideZIndex; + } + } else { + other + ..layer = parent.outsideLayer + ..zIndex = parent.outsideZIndex; + } + } +} diff --git a/packages/pinball_components/lib/src/components/layer_sensor/layer_sensor.dart b/packages/pinball_components/lib/src/components/layer_sensor/layer_sensor.dart new file mode 100644 index 00000000..4b1d6ae3 --- /dev/null +++ b/packages/pinball_components/lib/src/components/layer_sensor/layer_sensor.dart @@ -0,0 +1,66 @@ +// ignore_for_file: avoid_renaming_method_parameters, public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart'; + +/// {@template layer_entrance_orientation} +/// Determines if a layer entrance is oriented [up] or [down] on the board. +/// {@endtemplate} +enum LayerEntranceOrientation { + /// Facing up on the Board. + up, + + /// Facing down on the Board. + down, +} + +/// {@template layer_sensor} +/// [BodyComponent] located at the entrance and exit of a [Layer]. +/// +/// By default the base [layer] is set to [Layer.board] and the +/// [outsideZIndex] is set to [ZIndexes.ballOnBoard]. +/// {@endtemplate} +abstract class LayerSensor extends BodyComponent with InitialPosition, Layered { + /// {@macro layer_sensor} + LayerSensor({ + required this.insideLayer, + Layer? outsideLayer, + required this.insideZIndex, + int? outsideZIndex, + required this.orientation, + }) : outsideLayer = outsideLayer ?? Layer.board, + outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard, + super( + renderBody: false, + children: [LayerFilteringBehavior()], + ) { + layer = Layer.opening; + } + + final Layer insideLayer; + + final Layer outsideLayer; + + final int insideZIndex; + + final int outsideZIndex; + + /// The [Shape] of the [LayerSensor]. + Shape get shape; + + /// {@macro layer_entrance_orientation} + // TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for + // collision calculations. + final LayerEntranceOrientation orientation; + + @override + Body createBody() { + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef(position: initialPosition); + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/packages/pinball_components/test/src/components/layer_sensor/behavior/layer_filtering_behavior_test.dart b/packages/pinball_components/test/src/components/layer_sensor/behavior/layer_filtering_behavior_test.dart new file mode 100644 index 00000000..b7bc308b --- /dev/null +++ b/packages/pinball_components/test/src/components/layer_sensor/behavior/layer_filtering_behavior_test.dart @@ -0,0 +1,136 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/layer_sensor/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _TestLayerSensor extends LayerSensor { + _TestLayerSensor({ + required LayerEntranceOrientation orientation, + required int insideZIndex, + required Layer insideLayer, + }) : super( + insideLayer: insideLayer, + insideZIndex: insideZIndex, + orientation: orientation, + ); + + @override + Shape get shape => PolygonShape()..setAsBoxXY(1, 1); +} + +class _MockBall extends Mock implements Ball {} + +class _MockBody extends Mock implements Body {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'LayerSensorBehavior', + () { + test('can be instantiated', () { + expect( + LayerFilteringBehavior(), + isA(), + ); + }); + + flameTester.test( + 'loads', + (game) async { + final behavior = LayerFilteringBehavior(); + final parent = _TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insideZIndex: 1, + insideLayer: Layer.spaceshipEntranceRamp, + ); + + await parent.add(behavior); + await game.ensureAdd(parent); + + expect(game.contains(parent), isTrue); + }, + ); + + group('beginContact', () { + late Ball ball; + late Body body; + late int insideZIndex; + late Layer insideLayer; + + setUp(() { + ball = _MockBall(); + body = _MockBody(); + insideZIndex = 1; + insideLayer = Layer.spaceshipEntranceRamp; + + when(() => ball.body).thenReturn(body); + when(() => ball.layer).thenReturn(Layer.board); + }); + + flameTester.test( + 'changes ball layer and zIndex ' + 'when a ball enters and exits a downward oriented LayerSensor', + (game) async { + final parent = _TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insideZIndex: 1, + insideLayer: insideLayer, + )..initialPosition = Vector2(0, 10); + final behavior = LayerFilteringBehavior(); + + await parent.add(behavior); + await game.ensureAdd(parent); + + when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); + + behavior.beginContact(ball, _MockContact()); + verify(() => ball.layer = insideLayer).called(1); + verify(() => ball.zIndex = insideZIndex).called(1); + + when(() => ball.layer).thenReturn(insideLayer); + + behavior.beginContact(ball, _MockContact()); + verify(() => ball.layer = Layer.board); + verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); + }); + + flameTester.test( + 'changes ball layer and zIndex ' + 'when a ball enters and exits an upward oriented LayerSensor', + (game) async { + final parent = _TestLayerSensor( + orientation: LayerEntranceOrientation.up, + insideZIndex: 1, + insideLayer: insideLayer, + )..initialPosition = Vector2(0, 10); + final behavior = LayerFilteringBehavior(); + + await parent.add(behavior); + await game.ensureAdd(parent); + + when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); + + behavior.beginContact(ball, _MockContact()); + verify(() => ball.layer = insideLayer).called(1); + verify(() => ball.zIndex = 1).called(1); + + when(() => ball.layer).thenReturn(insideLayer); + + behavior.beginContact(ball, _MockContact()); + verify(() => ball.layer = Layer.board); + verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); + }); + }); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/layer_sensor_test.dart b/packages/pinball_components/test/src/components/layer_sensor/layer_sensor_test.dart similarity index 59% rename from packages/pinball_components/test/src/components/layer_sensor_test.dart rename to packages/pinball_components/test/src/components/layer_sensor/layer_sensor_test.dart index cfd19bb0..dd32ad56 100644 --- a/packages/pinball_components/test/src/components/layer_sensor_test.dart +++ b/packages/pinball_components/test/src/components/layer_sensor/layer_sensor_test.dart @@ -2,16 +2,10 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/layer_sensor/behaviors/behaviors.dart'; -import '../../helpers/helpers.dart'; - -class _MockBall extends Mock implements Ball {} - -class _MockBody extends Mock implements Body {} - -class _MockContact extends Mock implements Contact {} +import '../../../helpers/helpers.dart'; class TestLayerSensor extends LayerSensor { TestLayerSensor({ @@ -112,68 +106,22 @@ void main() { ); }); }); - }); - - group('beginContact', () { - late Ball ball; - late Body body; - late int insideZIndex; - late Layer insideLayer; - - setUp(() { - ball = _MockBall(); - body = _MockBody(); - insideZIndex = 1; - insideLayer = Layer.spaceshipEntranceRamp; - - when(() => ball.body).thenReturn(body); - when(() => ball.layer).thenReturn(Layer.board); - }); - - flameTester.test( - 'changes ball layer and zIndex ' - 'when a ball enters and exits a downward oriented LayerSensor', - (game) async { - final sensor = TestLayerSensor( - orientation: LayerEntranceOrientation.down, - insideZIndex: insidePriority, - insideLayer: insideLayer, - )..initialPosition = Vector2(0, 10); - - when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); - - sensor.beginContact(ball, _MockContact()); - verify(() => ball.layer = insideLayer).called(1); - verify(() => ball.zIndex = insideZIndex).called(1); - - when(() => ball.layer).thenReturn(insideLayer); - - sensor.beginContact(ball, _MockContact()); - verify(() => ball.layer = Layer.board); - verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); - }); flameTester.test( - 'changes ball layer and zIndex ' - 'when a ball enters and exits an upward oriented LayerSensor', - (game) async { - final sensor = TestLayerSensor( - orientation: LayerEntranceOrientation.up, - insideZIndex: insidePriority, - insideLayer: insideLayer, - )..initialPosition = Vector2(0, 10); - - when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - - sensor.beginContact(ball, _MockContact()); - verify(() => ball.layer = insideLayer).called(1); - verify(() => ball.zIndex = insidePriority).called(1); - - when(() => ball.layer).thenReturn(insideLayer); + 'adds a LayerFilteringBehavior', + (game) async { + final layerSensor = TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insideZIndex: insidePriority, + insideLayer: Layer.spaceshipEntranceRamp, + ); + await game.ensureAdd(layerSensor); - sensor.beginContact(ball, _MockContact()); - verify(() => ball.layer = Layer.board); - verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); - }); + expect( + layerSensor.children.whereType().length, + equals(1), + ); + }, + ); }); } From 155e316ba1cba5e08208626cada33adfe8b35fda Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Wed, 4 May 2022 20:43:48 -0500 Subject: [PATCH 09/10] feat: implemented `SkillShot` (#337) * feat: add skill shot * fix: unused import * refactor: switched method * style: parameter order --- lib/game/game_assets.dart | 4 + lib/game/pinball_game.dart | 6 + .../assets/images/skill_shot/decal.png | Bin 0 -> 96796 bytes .../assets/images/skill_shot/dimmed.png | Bin 0 -> 9522 bytes .../assets/images/skill_shot/lit.png | Bin 0 -> 10179 bytes .../assets/images/skill_shot/pin.png | Bin 0 -> 2333 bytes .../lib/gen/assets.gen.dart | 21 +++ .../chrome_dino/cubit/chrome_dino_cubit.dart | 2 +- .../chrome_dino/cubit/chrome_dino_state.dart | 2 +- .../lib/src/components/components.dart | 1 + .../skill_shot/behaviors/behaviors.dart | 2 + .../skill_shot_ball_contact_behavior.dart | 16 ++ .../skill_shot_blinking_behavior.dart | 44 +++++ .../skill_shot/cubit/skill_shot_cubit.dart | 39 ++++ .../skill_shot/cubit/skill_shot_state.dart | 37 ++++ .../src/components/skill_shot/skill_shot.dart | 169 ++++++++++++++++++ packages/pinball_components/pubspec.yaml | 1 + .../chrome_dino_swiveling_behavior_test.dart | 10 +- .../chrome_dino/chrome_dino_test.dart | 2 +- .../cubit/chrome_dino_cubit_test.dart | 2 +- .../cubit/chrome_dino_state_test.dart | 2 +- ...skill_shot_ball_contact_behavior_test.dart | 62 +++++++ .../skill_shot_blinking_behavior_test.dart | 125 +++++++++++++ .../cubit/skill_shot_cubit_test.dart | 66 +++++++ .../cubit/skill_shot_state_test.dart | 84 +++++++++ .../skill_shot/skill_shot_test.dart | 99 ++++++++++ test/game/pinball_game_test.dart | 32 +++- 27 files changed, 811 insertions(+), 17 deletions(-) create mode 100644 packages/pinball_components/assets/images/skill_shot/decal.png create mode 100644 packages/pinball_components/assets/images/skill_shot/dimmed.png create mode 100644 packages/pinball_components/assets/images/skill_shot/lit.png create mode 100644 packages/pinball_components/assets/images/skill_shot/pin.png create mode 100644 packages/pinball_components/lib/src/components/skill_shot/behaviors/behaviors.dart create mode 100644 packages/pinball_components/lib/src/components/skill_shot/behaviors/skill_shot_ball_contact_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/skill_shot/behaviors/skill_shot_blinking_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/skill_shot/cubit/skill_shot_cubit.dart create mode 100644 packages/pinball_components/lib/src/components/skill_shot/cubit/skill_shot_state.dart create mode 100644 packages/pinball_components/lib/src/components/skill_shot/skill_shot.dart create mode 100644 packages/pinball_components/test/src/components/skill_shot/behaviors/skill_shot_ball_contact_behavior_test.dart create mode 100644 packages/pinball_components/test/src/components/skill_shot/behaviors/skill_shot_blinking_behavior_test.dart create mode 100644 packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_cubit_test.dart create mode 100644 packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_state_test.dart create mode 100644 packages/pinball_components/test/src/components/skill_shot/skill_shot_test.dart diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index d066ce0d..ac324417 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -131,6 +131,10 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.flapper.backSupport.keyName), images.load(components.Assets.images.flapper.frontSupport.keyName), images.load(components.Assets.images.flapper.flap.keyName), + images.load(components.Assets.images.skillShot.decal.keyName), + images.load(components.Assets.images.skillShot.pin.keyName), + images.load(components.Assets.images.skillShot.lit.keyName), + images.load(components.Assets.images.skillShot.dimmed.keyName), images.load(dashTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index aa963a53..bdf23759 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -7,6 +7,7 @@ import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_audio/pinball_audio.dart'; @@ -57,6 +58,11 @@ class PinballGame extends PinballForge2DGame GoogleWord(position: Vector2(-4.25, 1.8)), Multipliers(), Multiballs(), + SkillShot( + children: [ + ScoringContactBehavior(points: Points.oneMillion), + ], + ), ]; final characterAreas = [ AndroidAcres(), diff --git a/packages/pinball_components/assets/images/skill_shot/decal.png b/packages/pinball_components/assets/images/skill_shot/decal.png new file mode 100644 index 0000000000000000000000000000000000000000..120d70aaae161c3639c0476855b3ce53db87b706 GIT binary patch literal 96796 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/skill_shot/pin.png b/packages/pinball_components/assets/images/skill_shot/pin.png new file mode 100644 index 0000000000000000000000000000000000000000..5b64e1ab10b92860db22cc7c89a345ab4f188810 GIT binary patch literal 2333 zcmV+&3F7vNP){D0f4^{aEMrIwc2kat5|DwQArIOy9t)gQpPrfd)ysk_*X#8+%jFo?*EhB& zdA=C(LPVT<`xo#0>D^zy`{5f`-}o&6$P0%{nJ<+}u(G_oXeje31HndB3vh=~?CkFihxY=T zecWg^-I+)ARH5SBj$0@4x^4q4&NYG=GU)nxB8Iq2BxDrKNkIy8puAL}aeh=`_aUkuEJR zeFp&J5XZ%Fe6`hTroCRzcDvm>0AK?-N>LQOj)>7&a2UtQ)!BMI9SnAgot;6b5w6vy zZq{nGXmxFUG#m~?i-BA1&c}cJ^wYn!+N}=`OV4p|T&>k^R?|w<>pgm$W!e3}Vla(n z^JbF7w6e0)FYYip~IymKpoM$&e> za}@xfyVhM))gaL(=DEw4FJCLH#o^=q)5D60)mFRxCV;|vcYQ%s2Z2VKow>QWEE{WY zVDx)4RKS=gCv7 zr0I?6>FLtu=H_UBw14+BIED{;t-MvMRZG2IZ$BH4Lq*%n z&em_oaV*_#ciR@l;%Q;UN+rFKrfF$wb89%svIn6?W@ct@B}pQ^jg2km+;W(LVA5>0 zuM;73*SgF8h-e>$lS-Oi9}EV3@BaOsbFLexQ%SScx*;Oa-|9aM&Qrpf&RplU$z-Cv zUhn_%^eCJuSJKPd+kNcy9<8hDUYHS@uGbr{_4|EYTU)yuDB2*#%r_g2dX{B&cW39D zqj^e4N;Roe$}J-Td6snnAP=onVw$>t|A8G01`k4uAx@HOQ&Usr{#Ji99_`;dD%#J$ zAwZlYaV?HZYHi*REe2s}3rl(b@h~s)$x>jRa=B8uHXIJ+?%liFMNurD+FKDZj^ns2 zBI=ym37y5nNHa~#VxlPX-fsuysg%o=D?2+o)a&(ns=9j?Tse+Q5h8i-{o_C*C`RgW zDPlyLsOm8Axu{$zccREZkr%z=^7JTN!pu?>xms(p!0@0kW=cfJJDaQOIP5P$shlKJ zaa>aCitXc4h>py02@)IUy*lT|fgT_hNf8n9JkM3b#v}xcqEeJH5!j;GISUREF(E{; z)_U(xlMPKk)Ewd%bO4J(P5DyTwXZ5}#c zB4BL981KDWTV!X#A!3z?5CFV-%|bmFm7-Ju(H_7BdJYpnFwQ!4?)WG@6IK>ZnTc#+ zo%cQul>`wf11J=EK2Z$|DMAUp5)rwBu#V3PD~Xtth`>2#H4qK}B4R120zA>iAmL`_su4rphj|W-F;zlD z>#PfgLrjdY0Z_Hpg~|^wa|)p3ymNX&@g0U^V@!pa!82L^!nG*m2=j98owcF;7>S4w0mh?o@1%nwI@Ie- zsZ@%Q2y9_<4a_-asv2X+dsXjzXv?!jp8w#S8?COae0kDzl^BsH4+f7*%#t!QI_IA%edyA&96*Qmo{6v+RXq# zSS4aoRj@9&>d2TVjZB29aAJ>qZqG4GDk5a9Ra+FHaKcOp5rVVUzO1k!5vIa~UeDLS zMT7tm_V*w6opb${0>>g%BtrGxKYV3Adyz-@g&P$P#M3{^*Rc1!7UVXUI zPr9qCfBEX~U;XfKVd3+@Bii%BiAXBUaIj|$&K=FN?BD-=@Zdk^xDYvaSSgB1sfa-Z z_$f;N!od#1u?Sa;2*8iKp;z)rPzanc#*~Q=+`*0`gA1_e#}VBm%yKX`dq252aEOLP z5**VvP3$`xYN2C7(=1` const $AssetsImagesPlungerGen(); $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); + $AssetsImagesSkillShotGen get skillShot => const $AssetsImagesSkillShotGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); } @@ -272,6 +273,26 @@ class $AssetsImagesSignpostGen { const AssetGenImage('assets/images/signpost/inactive.png'); } +class $AssetsImagesSkillShotGen { + const $AssetsImagesSkillShotGen(); + + /// File path: assets/images/skill_shot/decal.png + AssetGenImage get decal => + const AssetGenImage('assets/images/skill_shot/decal.png'); + + /// File path: assets/images/skill_shot/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/skill_shot/dimmed.png'); + + /// File path: assets/images/skill_shot/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/skill_shot/lit.png'); + + /// File path: assets/images/skill_shot/pin.png + AssetGenImage get pin => + const AssetGenImage('assets/images/skill_shot/pin.png'); +} + class $AssetsImagesSlingshotGen { const $AssetsImagesSlingshotGen(); diff --git a/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart index 649e804b..06e34199 100644 --- a/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart +++ b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart @@ -7,7 +7,7 @@ import 'package:pinball_components/pinball_components.dart'; part 'chrome_dino_state.dart'; class ChromeDinoCubit extends Cubit { - ChromeDinoCubit() : super(const ChromeDinoState.inital()); + ChromeDinoCubit() : super(const ChromeDinoState.initial()); void onOpenMouth() { emit(state.copyWith(isMouthOpen: true)); diff --git a/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart index a5d3b183..8ed6fa8c 100644 --- a/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart +++ b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart @@ -14,7 +14,7 @@ class ChromeDinoState extends Equatable { this.ball, }); - const ChromeDinoState.inital() + const ChromeDinoState.initial() : this( status: ChromeDinoStatus.idle, isMouthOpen: false, diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 5eef3538..db2f7d38 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -29,6 +29,7 @@ export 'rocket.dart'; export 'score_component.dart'; export 'shapes/shapes.dart'; export 'signpost/signpost.dart'; +export 'skill_shot/skill_shot.dart'; export 'slingshot.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp/spaceship_ramp.dart'; diff --git a/packages/pinball_components/lib/src/components/skill_shot/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/skill_shot/behaviors/behaviors.dart new file mode 100644 index 00000000..03aa31bd --- /dev/null +++ b/packages/pinball_components/lib/src/components/skill_shot/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'skill_shot_ball_contact_behavior.dart'; +export 'skill_shot_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/skill_shot/behaviors/skill_shot_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/skill_shot/behaviors/skill_shot_ball_contact_behavior.dart new file mode 100644 index 00000000..62e4185f --- /dev/null +++ b/packages/pinball_components/lib/src/components/skill_shot/behaviors/skill_shot_ball_contact_behavior.dart @@ -0,0 +1,16 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class SkillShotBallContactBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + parent.firstChild()?.playing = true; + } +} diff --git a/packages/pinball_components/lib/src/components/skill_shot/behaviors/skill_shot_blinking_behavior.dart b/packages/pinball_components/lib/src/components/skill_shot/behaviors/skill_shot_blinking_behavior.dart new file mode 100644 index 00000000..ea62fc25 --- /dev/null +++ b/packages/pinball_components/lib/src/components/skill_shot/behaviors/skill_shot_blinking_behavior.dart @@ -0,0 +1,44 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template skill_shot_blinking_behavior} +/// Makes a [SkillShot] blink between [SkillShotSpriteState.lit] and +/// [SkillShotSpriteState.dimmed] for a set amount of blinks. +/// {@endtemplate} +class SkillShotBlinkingBehavior extends TimerComponent + with ParentIsA { + /// {@macro skill_shot_blinking_behavior} + SkillShotBlinkingBehavior() : super(period: 0.15); + + final _maxBlinks = 4; + int _blinks = 0; + + void _onNewState(SkillShotState state) { + if (state.isBlinking) { + timer + ..reset() + ..start(); + } + } + + @override + Future onLoad() async { + await super.onLoad(); + timer.stop(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + if (_blinks != _maxBlinks * 2) { + parent.bloc.switched(); + _blinks++; + } else { + _blinks = 0; + timer.stop(); + parent.bloc.onBlinkingFinished(); + } + } +} diff --git a/packages/pinball_components/lib/src/components/skill_shot/cubit/skill_shot_cubit.dart b/packages/pinball_components/lib/src/components/skill_shot/cubit/skill_shot_cubit.dart new file mode 100644 index 00000000..b9491385 --- /dev/null +++ b/packages/pinball_components/lib/src/components/skill_shot/cubit/skill_shot_cubit.dart @@ -0,0 +1,39 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'skill_shot_state.dart'; + +class SkillShotCubit extends Cubit { + SkillShotCubit() : super(const SkillShotState.initial()); + + void onBallContacted() { + emit( + const SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: true, + ), + ); + } + + void switched() { + switch (state.spriteState) { + case SkillShotSpriteState.lit: + emit(state.copyWith(spriteState: SkillShotSpriteState.dimmed)); + break; + case SkillShotSpriteState.dimmed: + emit(state.copyWith(spriteState: SkillShotSpriteState.lit)); + break; + } + } + + void onBlinkingFinished() { + emit( + const SkillShotState( + spriteState: SkillShotSpriteState.dimmed, + isBlinking: false, + ), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/skill_shot/cubit/skill_shot_state.dart b/packages/pinball_components/lib/src/components/skill_shot/cubit/skill_shot_state.dart new file mode 100644 index 00000000..1e040db6 --- /dev/null +++ b/packages/pinball_components/lib/src/components/skill_shot/cubit/skill_shot_state.dart @@ -0,0 +1,37 @@ +// ignore_for_file: public_member_api_docs + +part of 'skill_shot_cubit.dart'; + +enum SkillShotSpriteState { + lit, + dimmed, +} + +class SkillShotState extends Equatable { + const SkillShotState({ + required this.spriteState, + required this.isBlinking, + }); + + const SkillShotState.initial() + : this( + spriteState: SkillShotSpriteState.dimmed, + isBlinking: false, + ); + + final SkillShotSpriteState spriteState; + + final bool isBlinking; + + SkillShotState copyWith({ + SkillShotSpriteState? spriteState, + bool? isBlinking, + }) => + SkillShotState( + spriteState: spriteState ?? this.spriteState, + isBlinking: isBlinking ?? this.isBlinking, + ); + + @override + List get props => [spriteState, isBlinking]; +} diff --git a/packages/pinball_components/lib/src/components/skill_shot/skill_shot.dart b/packages/pinball_components/lib/src/components/skill_shot/skill_shot.dart new file mode 100644 index 00000000..3bf10a7e --- /dev/null +++ b/packages/pinball_components/lib/src/components/skill_shot/skill_shot.dart @@ -0,0 +1,169 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/skill_shot_cubit.dart'; + +/// {@template skill_shot} +/// Rollover awarding extra points. +/// {@endtemplate} +class SkillShot extends BodyComponent with ZIndex { + /// {@macro skill_shot} + SkillShot({Iterable? children}) + : this._( + children: children, + bloc: SkillShotCubit(), + ); + + SkillShot._({ + Iterable? children, + required this.bloc, + }) : super( + renderBody: false, + children: [ + SkillShotBallContactBehavior(), + SkillShotBlinkingBehavior(), + _RolloverDecalSpriteComponent(), + PinSpriteAnimationComponent(), + _TextDecalSpriteGroupComponent(state: bloc.state.spriteState), + ...?children, + ], + ) { + zIndex = ZIndexes.decal; + } + + /// Creates a [SkillShot] without any children. + /// + /// This can be used for testing [SkillShot]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + SkillShot.test({ + required this.bloc, + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final SkillShotCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = PolygonShape() + ..setAsBox( + 0.1, + 3.7, + Vector2(-31.9, 9.1), + 0.11, + ); + final fixtureDef = FixtureDef(shape, isSensor: true); + return world.createBody(BodyDef())..createFixture(fixtureDef); + } +} + +class _RolloverDecalSpriteComponent extends SpriteComponent with HasGameRef { + _RolloverDecalSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-31.9, 9.1), + angle: 0.11, + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.skillShot.decal.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 20; + } +} + +/// {@template pin_sprite_animation_component} +/// Animation for pin in [SkillShot] rollover. +/// {@endtemplate} +@visibleForTesting +class PinSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef { + /// {@macro pin_sprite_animation_component} + PinSpriteAnimationComponent() + : super( + anchor: Anchor.center, + position: Vector2(-31.9, 9.1), + angle: 0, + playing: false, + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.skillShot.pin.keyName, + ); + + const amountPerRow = 3; + const amountPerColumn = 1; + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + loop: false, + ), + )..onComplete = () { + animation?.reset(); + playing = false; + }; + } +} + +class _TextDecalSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _TextDecalSpriteGroupComponent({ + required SkillShotSpriteState state, + }) : super( + anchor: Anchor.center, + position: Vector2(-35.55, 3.59), + current: state, + ); + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state.spriteState); + + final sprites = { + SkillShotSpriteState.lit: Sprite( + gameRef.images.fromCache(Assets.images.skillShot.lit.keyName), + ), + SkillShotSpriteState.dimmed: Sprite( + gameRef.images.fromCache(Assets.images.skillShot.dimmed.keyName), + ), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index bee6fd02..4f66c220 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -89,6 +89,7 @@ flutter: - assets/images/score/ - assets/images/backbox/ - assets/images/flapper/ + - assets/images/skill_shot/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart index 9b6a05b6..4b34940c 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart @@ -36,7 +36,7 @@ void main() { whenListen( bloc, const Stream.empty(), - initialState: const ChromeDinoState.inital(), + initialState: const ChromeDinoState.initial(), ); final chromeDino = ChromeDino.test(bloc: bloc); @@ -58,7 +58,7 @@ void main() { whenListen( bloc, const Stream.empty(), - initialState: const ChromeDinoState.inital(), + initialState: const ChromeDinoState.initial(), ); final chromeDino = ChromeDino.test(bloc: bloc); @@ -91,7 +91,7 @@ void main() { bloc, const Stream.empty(), initialState: - const ChromeDinoState.inital().copyWith(isMouthOpen: true), + const ChromeDinoState.initial().copyWith(isMouthOpen: true), ); final chromeDino = ChromeDino.test(bloc: bloc); @@ -120,7 +120,7 @@ void main() { bloc, const Stream.empty(), initialState: - const ChromeDinoState.inital().copyWith(isMouthOpen: false), + const ChromeDinoState.initial().copyWith(isMouthOpen: false), ); final chromeDino = ChromeDino.test(bloc: bloc); @@ -148,7 +148,7 @@ void main() { bloc, const Stream.empty(), initialState: - const ChromeDinoState.inital().copyWith(isMouthOpen: false), + const ChromeDinoState.initial().copyWith(isMouthOpen: false), ); final chromeDino = ChromeDino.test(bloc: bloc); diff --git a/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart b/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart index 4c1802ef..d6366092 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart @@ -79,7 +79,7 @@ void main() { whenListen( bloc, const Stream.empty(), - initialState: const ChromeDinoState.inital(), + initialState: const ChromeDinoState.initial(), ); when(bloc.close).thenAnswer((_) async {}); final chromeDino = ChromeDino.test(bloc: bloc); diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart index 79375a6e..80c01983 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart @@ -57,7 +57,7 @@ void main() { blocTest( 'onChomp emits nothing when the ball is already in the mouth', build: ChromeDinoCubit.new, - seed: () => const ChromeDinoState.inital().copyWith(ball: ball), + seed: () => const ChromeDinoState.initial().copyWith(ball: ball), act: (bloc) => bloc.onChomp(ball), expect: () => [], ); diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart index 442d544b..0d7f9c83 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart @@ -36,7 +36,7 @@ void main() { status: ChromeDinoStatus.idle, isMouthOpen: false, ); - expect(ChromeDinoState.inital(), equals(initialState)); + expect(ChromeDinoState.initial(), equals(initialState)); }); }); diff --git a/packages/pinball_components/test/src/components/skill_shot/behaviors/skill_shot_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/skill_shot/behaviors/skill_shot_ball_contact_behavior_test.dart new file mode 100644 index 00000000..48a151a3 --- /dev/null +++ b/packages/pinball_components/test/src/components/skill_shot/behaviors/skill_shot_ball_contact_behavior_test.dart @@ -0,0 +1,62 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + +class _MockSkillShotCubit extends Mock implements SkillShotCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'SkillShotBallContactBehavior', + () { + test('can be instantiated', () { + expect( + SkillShotBallContactBehavior(), + isA(), + ); + }); + + flameTester.testGameWidget( + 'beginContact animates pin and calls onBallContacted ' + 'when contacts with a ball', + setUp: (game, tester) async { + await game.images.load(Assets.images.skillShot.pin.keyName); + final behavior = SkillShotBallContactBehavior(); + final bloc = _MockSkillShotCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SkillShotState.initial(), + ); + + final skillShot = SkillShot.test(bloc: bloc); + await skillShot.addAll([behavior, PinSpriteAnimationComponent()]); + await game.ensureAdd(skillShot); + + behavior.beginContact(_MockBall(), _MockContact()); + await tester.pump(); + + expect( + skillShot.firstChild()!.playing, + isTrue, + ); + verify(skillShot.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/skill_shot/behaviors/skill_shot_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/skill_shot/behaviors/skill_shot_blinking_behavior_test.dart new file mode 100644 index 00000000..e2d00f61 --- /dev/null +++ b/packages/pinball_components/test/src/components/skill_shot/behaviors/skill_shot_blinking_behavior_test.dart @@ -0,0 +1,125 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockSkillShotCubit extends Mock implements SkillShotCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'SkillShotBlinkingBehavior', + () { + flameTester.testGameWidget( + 'calls switched after 0.15 seconds when isBlinking and lit', + setUp: (game, tester) async { + final behavior = SkillShotBlinkingBehavior(); + final bloc = _MockSkillShotCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: const SkillShotState.initial(), + ); + + final skillShot = SkillShot.test(bloc: bloc); + await skillShot.add(behavior); + await game.ensureAdd(skillShot); + + streamController.add( + const SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: true, + ), + ); + await tester.pump(); + game.update(0.15); + + await streamController.close(); + verify(bloc.switched).called(1); + }, + ); + + flameTester.testGameWidget( + 'calls switched after 0.15 seconds when isBlinking and dimmed', + setUp: (game, tester) async { + final behavior = SkillShotBlinkingBehavior(); + final bloc = _MockSkillShotCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: const SkillShotState.initial(), + ); + + final skillShot = SkillShot.test(bloc: bloc); + await skillShot.add(behavior); + await game.ensureAdd(skillShot); + + streamController.add( + const SkillShotState( + spriteState: SkillShotSpriteState.dimmed, + isBlinking: true, + ), + ); + await tester.pump(); + game.update(0.15); + + await streamController.close(); + verify(bloc.switched).called(1); + }, + ); + + flameTester.testGameWidget( + 'calls onBlinkingFinished after all blinks complete', + setUp: (game, tester) async { + final behavior = SkillShotBlinkingBehavior(); + final bloc = _MockSkillShotCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: const SkillShotState.initial(), + ); + + final skillShot = SkillShot.test(bloc: bloc); + await skillShot.add(behavior); + await game.ensureAdd(skillShot); + + for (var i = 0; i <= 8; i++) { + if (i.isEven) { + streamController.add( + const SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: true, + ), + ); + } else { + streamController.add( + const SkillShotState( + spriteState: SkillShotSpriteState.dimmed, + isBlinking: true, + ), + ); + } + await tester.pump(); + game.update(0.15); + } + + await streamController.close(); + verify(bloc.onBlinkingFinished).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_cubit_test.dart b/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_cubit_test.dart new file mode 100644 index 00000000..b165db99 --- /dev/null +++ b/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_cubit_test.dart @@ -0,0 +1,66 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'SkillShotCubit', + () { + blocTest( + 'onBallContacted emits lit and true', + build: SkillShotCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [ + SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: true, + ), + ], + ); + + blocTest( + 'switched emits lit when dimmed', + build: SkillShotCubit.new, + act: (bloc) => bloc.switched(), + expect: () => [ + isA().having( + (state) => state.spriteState, + 'spriteState', + SkillShotSpriteState.lit, + ) + ], + ); + + blocTest( + 'switched emits dimmed when lit', + build: SkillShotCubit.new, + seed: () => SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: false, + ), + act: (bloc) => bloc.switched(), + expect: () => [ + isA().having( + (state) => state.spriteState, + 'spriteState', + SkillShotSpriteState.dimmed, + ) + ], + ); + + blocTest( + 'onBlinkingFinished emits dimmed and false', + build: SkillShotCubit.new, + act: (bloc) => bloc.onBlinkingFinished(), + expect: () => [ + SkillShotState( + spriteState: SkillShotSpriteState.dimmed, + isBlinking: false, + ), + ], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_state_test.dart b/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_state_test.dart new file mode 100644 index 00000000..ee6e3e0d --- /dev/null +++ b/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_state_test.dart @@ -0,0 +1,84 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('SkillShotState', () { + test('supports value equality', () { + expect( + SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: true, + ), + equals( + const SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: true, + ), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + const SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: true, + ), + isNotNull, + ); + }); + + test('initial is idle with mouth closed', () { + const initialState = SkillShotState( + spriteState: SkillShotSpriteState.dimmed, + isBlinking: false, + ); + expect(SkillShotState.initial(), equals(initialState)); + }); + }); + + group('copyWith', () { + test( + 'copies correctly ' + 'when no argument specified', + () { + const chromeDinoState = SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: true, + ); + expect( + chromeDinoState.copyWith(), + equals(chromeDinoState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + const chromeDinoState = SkillShotState( + spriteState: SkillShotSpriteState.lit, + isBlinking: true, + ); + final otherSkillShotState = SkillShotState( + spriteState: SkillShotSpriteState.dimmed, + isBlinking: false, + ); + expect(chromeDinoState, isNot(equals(otherSkillShotState))); + + expect( + chromeDinoState.copyWith( + spriteState: SkillShotSpriteState.dimmed, + isBlinking: false, + ), + equals(otherSkillShotState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/skill_shot/skill_shot_test.dart b/packages/pinball_components/test/src/components/skill_shot/skill_shot_test.dart new file mode 100644 index 00000000..dabacc69 --- /dev/null +++ b/packages/pinball_components/test/src/components/skill_shot/skill_shot_test.dart @@ -0,0 +1,99 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +class _MockSkillShotCubit extends Mock implements SkillShotCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.skillShot.decal.keyName, + Assets.images.skillShot.pin.keyName, + Assets.images.skillShot.lit.keyName, + Assets.images.skillShot.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('SkillShot', () { + flameTester.test('loads correctly', (game) async { + final skillShot = SkillShot(); + await game.ensureAdd(skillShot); + expect(game.contains(skillShot), isTrue); + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = _MockSkillShotCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SkillShotState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + final skillShot = SkillShot.test(bloc: bloc); + + await game.ensureAdd(skillShot); + game.remove(skillShot); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final skillShot = SkillShot( + children: [component], + ); + await game.ensureAdd(skillShot); + expect(skillShot.children, contains(component)); + }); + + flameTester.test('a SkillShotBallContactBehavior', (game) async { + final skillShot = SkillShot(); + await game.ensureAdd(skillShot); + expect( + skillShot.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('a SkillShotBlinkingBehavior', (game) async { + final skillShot = SkillShot(); + await game.ensureAdd(skillShot); + expect( + skillShot.children.whereType().single, + isNotNull, + ); + }); + }); + + flameTester.test( + 'pin stops animating after animation completes', + (game) async { + final skillShot = SkillShot(); + await game.ensureAdd(skillShot); + + final pinSpriteAnimationComponent = + skillShot.firstChild()!; + + pinSpriteAnimationComponent.playing = true; + game.update( + pinSpriteAnimationComponent.animation!.totalDuration() + 0.1, + ); + + expect(pinSpriteAnimationComponent.playing, isFalse); + }, + ); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index b75b3147..e1ed3084 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -136,6 +136,10 @@ void main() { Assets.images.flapper.flap.keyName, Assets.images.flapper.backSupport.keyName, Assets.images.flapper.frontSupport.keyName, + Assets.images.skillShot.decal.keyName, + Assets.images.skillShot.pin.keyName, + Assets.images.skillShot.lit.keyName, + Assets.images.skillShot.dimmed.keyName, ]; late GameBloc gameBloc; @@ -195,13 +199,16 @@ void main() { }, ); - flameBlocTester.test('has one FlutterForest', (game) async { - await game.ready(); - expect( - game.descendants().whereType().length, - equals(1), - ); - }); + flameBlocTester.test( + 'has one FlutterForest', + (game) async { + await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); flameBlocTester.test( 'has only one Multiballs', @@ -226,6 +233,17 @@ void main() { }, ); + flameBlocTester.test( + 'one SkillShot', + (game) async { + await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + group('controller', () { group('listenWhen', () { flameTester.testGameWidget( From 5fb9a40e66987c5174c46837a44fa2d4dbb9ba96 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 5 May 2022 12:38:32 +0100 Subject: [PATCH 10/10] fix: rendering with `FilterQuality.medium` (#334) * feat: defined HighFilterQualityCanvas * refactor: removed trailing comma * refactor: started defining CanvasComponent * feat: implemented CanvasComponent * docs: fixed typos * docs: changed template name * fix: merge conflict * refactor: set filterQuality to Medium * refactor: removed nullable from typdef * test: updated tests to FilterQuality.medium --- lib/game/behaviors/scoring_behavior.dart | 15 +- .../flutter_forest_bonus_behavior.dart | 2 +- lib/game/pinball_game.dart | 31 +- packages/pinball_flame/lib/pinball_flame.dart | 2 +- .../pinball_flame/lib/src/canvas/canvas.dart | 2 + .../lib/src/canvas/canvas_component.dart | 47 +++ .../canvas_wrapper.dart} | 80 +--- .../lib/src/canvas/z_canvas_component.dart | 77 ++++ .../src/canvas/canvas_component_test.dart | 144 +++++++ .../test/src/canvas/canvas_wrapper_test.dart | 353 ++++++++++++++++ .../src/canvas/z_canvas_component_test.dart | 80 ++++ .../test/src/goldens/rendering/blue_red.png | Bin 0 -> 22364 bytes .../test/src/goldens/rendering/red_blue.png | Bin 0 -> 22395 bytes .../rendering/z_canvas_component_test.dart | 385 ------------------ test/game/pinball_game_test.dart | 80 ++-- 15 files changed, 790 insertions(+), 508 deletions(-) create mode 100644 packages/pinball_flame/lib/src/canvas/canvas.dart create mode 100644 packages/pinball_flame/lib/src/canvas/canvas_component.dart rename packages/pinball_flame/lib/src/{z_canvas_component.dart => canvas/canvas_wrapper.dart} (65%) create mode 100644 packages/pinball_flame/lib/src/canvas/z_canvas_component.dart create mode 100644 packages/pinball_flame/test/src/canvas/canvas_component_test.dart create mode 100644 packages/pinball_flame/test/src/canvas/canvas_wrapper_test.dart create mode 100644 packages/pinball_flame/test/src/canvas/z_canvas_component_test.dart create mode 100644 packages/pinball_flame/test/src/goldens/rendering/blue_red.png create mode 100644 packages/pinball_flame/test/src/goldens/rendering/red_blue.png delete mode 100644 packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart diff --git a/lib/game/behaviors/scoring_behavior.dart b/lib/game/behaviors/scoring_behavior.dart index 84597838..eddcb580 100644 --- a/lib/game/behaviors/scoring_behavior.dart +++ b/lib/game/behaviors/scoring_behavior.dart @@ -40,13 +40,14 @@ class ScoringBehavior extends Component with HasGameRef { @override Future onLoad() async { gameRef.read().add(Scored(points: _points.value)); - await gameRef.firstChild()!.add( - ScoreComponent( - points: _points, - position: _position, - effectController: _effectController, - ), - ); + final canvas = gameRef.descendants().whereType().single; + await canvas.add( + ScoreComponent( + points: _points, + position: _position, + effectController: _effectController, + ), + ); } } diff --git a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart index 8f1b46e8..c06e6f87 100644 --- a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -17,7 +17,7 @@ class FlutterForestBonusBehavior extends Component final bumpers = parent.children.whereType(); final signpost = parent.firstChild()!; final animatronic = parent.firstChild()!; - final canvas = gameRef.firstChild()!; + final canvas = gameRef.descendants().whereType().single; for (final bumper in bumpers) { // TODO(alestiago): Refactor subscription management once the following is diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index bdf23759..907687c9 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -72,14 +72,23 @@ class PinballGame extends PinballForge2DGame ]; await add( - ZCanvasComponent( + CanvasComponent( + onSpritePainted: (paint) { + if (paint.filterQuality != FilterQuality.medium) { + paint.filterQuality = FilterQuality.medium; + } + }, children: [ - ...machine, - ...decals, - ...characterAreas, - Drain(), - BottomGroup(), - Launcher(), + ZCanvasComponent( + children: [ + ...machine, + ...decals, + ...characterAreas, + Drain(), + BottomGroup(), + Launcher(), + ], + ), ], ), ); @@ -169,7 +178,7 @@ class _GameBallsController extends ComponentController plunger.body.position.x, plunger.body.position.y - Ball.size.y, ); - component.firstChild()?.add(ball); + component.descendants().whereType().single.add(ball); }); } } @@ -203,9 +212,10 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { super.onTapUp(pointerId, info); if (info.raw.kind == PointerDeviceKind.mouse) { + final canvas = descendants().whereType().single; final ball = ControlledBall.debug() ..initialPosition = info.eventPosition.game; - firstChild()?.add(ball); + canvas.add(ball); } } @@ -230,10 +240,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { } void _turboChargeBall(Vector2 line) { + final canvas = descendants().whereType().single; final ball = ControlledBall.debug()..initialPosition = lineStart!; final impulse = line * -1 * 10; ball.add(BallTurboChargingBehavior(impulse: impulse)); - firstChild()?.add(ball); + canvas.add(ball); } } diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart index 8d458574..6f8a40f7 100644 --- a/packages/pinball_flame/lib/pinball_flame.dart +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -1,9 +1,9 @@ library pinball_flame; +export 'src/canvas/canvas.dart'; export 'src/component_controller.dart'; export 'src/contact_behavior.dart'; export 'src/keyboard_input_controller.dart'; export 'src/parent_is_a.dart'; export 'src/pinball_forge2d_game.dart'; export 'src/sprite_animation.dart'; -export 'src/z_canvas_component.dart'; diff --git a/packages/pinball_flame/lib/src/canvas/canvas.dart b/packages/pinball_flame/lib/src/canvas/canvas.dart new file mode 100644 index 00000000..9c0c7a70 --- /dev/null +++ b/packages/pinball_flame/lib/src/canvas/canvas.dart @@ -0,0 +1,2 @@ +export 'canvas_component.dart'; +export 'z_canvas_component.dart'; diff --git a/packages/pinball_flame/lib/src/canvas/canvas_component.dart b/packages/pinball_flame/lib/src/canvas/canvas_component.dart new file mode 100644 index 00000000..ca6e64d0 --- /dev/null +++ b/packages/pinball_flame/lib/src/canvas/canvas_component.dart @@ -0,0 +1,47 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:pinball_flame/src/canvas/canvas_wrapper.dart'; + +/// Called right before [Canvas.drawImageRect] is called. +/// +/// This is useful since [Sprite.render] uses [Canvas.drawImageRect] to draw +/// the [Sprite]. +typedef PaintFunction = void Function(Paint); + +/// {@template canvas_component} +/// Allows listening before the rendering of [Sprite]s. +/// +/// The existance of this class is to hack around the fact that Flame doesn't +/// provide a global way to modify the default [Paint] before rendering a +/// [Sprite]. +/// {@endtemplate} +class CanvasComponent extends Component { + /// {@macro canvas_component} + CanvasComponent({ + PaintFunction? onSpritePainted, + Iterable? children, + }) : _canvas = _Canvas(onSpritePainted: onSpritePainted), + super(children: children); + + final _Canvas _canvas; + + @override + void renderTree(Canvas canvas) { + _canvas.canvas = canvas; + super.renderTree(_canvas); + } +} + +class _Canvas extends CanvasWrapper { + _Canvas({PaintFunction? onSpritePainted}) + : _onSpritePainted = onSpritePainted; + + final PaintFunction? _onSpritePainted; + + @override + void drawImageRect(Image image, Rect src, Rect dst, Paint paint) { + _onSpritePainted?.call(paint); + super.drawImageRect(image, src, dst, paint); + } +} diff --git a/packages/pinball_flame/lib/src/z_canvas_component.dart b/packages/pinball_flame/lib/src/canvas/canvas_wrapper.dart similarity index 65% rename from packages/pinball_flame/lib/src/z_canvas_component.dart rename to packages/pinball_flame/lib/src/canvas/canvas_wrapper.dart index 911c3e93..883527d2 100644 --- a/packages/pinball_flame/lib/src/z_canvas_component.dart +++ b/packages/pinball_flame/lib/src/canvas/canvas_wrapper.dart @@ -1,85 +1,11 @@ +// ignore_for_file: public_member_api_docs + import 'dart:typed_data'; import 'dart:ui'; -import 'package:flame/components.dart'; - -/// {@template z_canvas_component} -/// Draws [ZIndex] components after the all non-[ZIndex] components have been -/// drawn. -/// {@endtemplate} -class ZCanvasComponent extends Component { - /// {@macro z_canvas_component} - ZCanvasComponent({ - Iterable? children, - }) : _zCanvas = ZCanvas(), - super(children: children); - - final ZCanvas _zCanvas; - - @override - void renderTree(Canvas canvas) { - _zCanvas.canvas = canvas; - super.renderTree(_zCanvas); - _zCanvas.render(); - } -} - -/// Apply to any [Component] that will be rendered according to a -/// [ZIndex.zIndex]. -/// -/// [ZIndex] components must be descendants of a [ZCanvasComponent]. -/// -/// {@macro z_canvas.render} -mixin ZIndex on Component { - /// The z-index of this component. - /// - /// The higher the value, the later the component will be drawn. Hence, - /// rendering in front of [Component]s with lower [zIndex] values. - int zIndex = 0; - - @override - void renderTree( - Canvas canvas, - ) { - if (canvas is ZCanvas) { - canvas.buffer(this); - } else { - super.renderTree(canvas); - } - } -} - -/// The [ZCanvas] allows to postpone the rendering of [ZIndex] components. -/// -/// You should not use this class directly. -class ZCanvas implements Canvas { - /// The [Canvas] to render to. - /// - /// This is set by [ZCanvasComponent] when rendering. +class CanvasWrapper implements Canvas { late Canvas canvas; - final List _zBuffer = []; - - /// Postpones the rendering of [ZIndex] component and its children. - void buffer(ZIndex component) => _zBuffer.add(component); - - /// Renders all [ZIndex] components and their children. - /// - /// {@template z_canvas.render} - /// The rendering order is defined by the parent [ZIndex]. The children of - /// the same parent are rendered in the order they were added. - /// - /// If two [Component]s ever overlap each other, and have the same - /// [ZIndex.zIndex], there is no guarantee that the first one will be rendered - /// before the second one. - /// {@endtemplate} - void render() => _zBuffer - ..sort((a, b) => a.zIndex.compareTo(b.zIndex)) - ..whereType().forEach(_render) - ..clear(); - - void _render(Component component) => component.renderTree(canvas); - @override void clipPath(Path path, {bool doAntiAlias = true}) => canvas.clipPath(path, doAntiAlias: doAntiAlias); diff --git a/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart b/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart new file mode 100644 index 00000000..e097f359 --- /dev/null +++ b/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart @@ -0,0 +1,77 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:pinball_flame/src/canvas/canvas_wrapper.dart'; + +/// {@template z_canvas_component} +/// Draws [ZIndex] components after the all non-[ZIndex] components have been +/// drawn. +/// {@endtemplate} +class ZCanvasComponent extends Component { + /// {@macro z_canvas_component} + ZCanvasComponent({ + Iterable? children, + }) : _zCanvas = _ZCanvas(), + super(children: children); + + final _ZCanvas _zCanvas; + + @override + void renderTree(Canvas canvas) { + _zCanvas.canvas = canvas; + super.renderTree(_zCanvas); + _zCanvas.render(); + } +} + +/// Apply to any [Component] that will be rendered according to a +/// [ZIndex.zIndex]. +/// +/// [ZIndex] components must be descendants of a [ZCanvasComponent]. +/// +/// {@macro z_canvas.render} +mixin ZIndex on Component { + /// The z-index of this component. + /// + /// The higher the value, the later the component will be drawn. Hence, + /// rendering in front of [Component]s with lower [zIndex] values. + int zIndex = 0; + + @override + void renderTree( + Canvas canvas, + ) { + if (canvas is _ZCanvas) { + canvas.buffer(this); + } else { + super.renderTree(canvas); + } + } +} + +/// The [_ZCanvas] allows to postpone the rendering of [ZIndex] components. +/// +/// You should not use this class directly. +class _ZCanvas extends CanvasWrapper { + final List _zBuffer = []; + + /// Postpones the rendering of [ZIndex] component and its children. + void buffer(ZIndex component) => _zBuffer.add(component); + + /// Renders all [ZIndex] components and their children. + /// + /// {@template z_canvas.render} + /// The rendering order is defined by the parent [ZIndex]. The children of + /// the same parent are rendered in the order they were added. + /// + /// If two [Component]s ever overlap each other, and have the same + /// [ZIndex.zIndex], there is no guarantee that the first one will be rendered + /// before the second one. + /// {@endtemplate} + void render() => _zBuffer + ..sort((a, b) => a.zIndex.compareTo(b.zIndex)) + ..whereType().forEach(_render) + ..clear(); + + void _render(Component component) => component.renderTree(canvas); +} diff --git a/packages/pinball_flame/test/src/canvas/canvas_component_test.dart b/packages/pinball_flame/test/src/canvas/canvas_component_test.dart new file mode 100644 index 00000000..7bf7fd88 --- /dev/null +++ b/packages/pinball_flame/test/src/canvas/canvas_component_test.dart @@ -0,0 +1,144 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_flame/src/canvas/canvas_component.dart'; + +class _TestSpriteComponent extends SpriteComponent {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CanvasComponent', () { + final flameTester = FlameTester(FlameGame.new); + + test('can be instantiated', () { + expect( + CanvasComponent(), + isA(), + ); + }); + + flameTester.test('loads correctly', (game) async { + final component = CanvasComponent(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.test( + 'adds children', + (game) async { + final component = Component(); + final canvas = CanvasComponent( + onSpritePainted: (paint) => paint.filterQuality = FilterQuality.high, + children: [component], + ); + + await game.ensureAdd(canvas); + + expect( + canvas.children.contains(component), + isTrue, + ); + }, + ); + + flameTester.testGameWidget( + 'calls onSpritePainted when paiting a sprite', + setUp: (game, tester) async { + final spriteComponent = _TestSpriteComponent(); + + final completer = Completer(); + decodeImageFromList( + Uint8List.fromList(_image), + completer.complete, + ); + spriteComponent.sprite = Sprite(await completer.future); + + var calls = 0; + final canvas = CanvasComponent( + onSpritePainted: (paint) => calls++, + children: [spriteComponent], + ); + + await game.ensureAdd(canvas); + await tester.pump(); + + expect(calls, equals(1)); + }, + ); + }); +} + +const List _image = [ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x0D, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, + 0x1F, + 0x15, + 0xC4, + 0x89, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x49, + 0x44, + 0x41, + 0x54, + 0x78, + 0x9C, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, + 0x0D, + 0x0A, + 0x2D, + 0xB4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4E, + 0x44, + 0xAE, +]; diff --git a/packages/pinball_flame/test/src/canvas/canvas_wrapper_test.dart b/packages/pinball_flame/test/src/canvas/canvas_wrapper_test.dart new file mode 100644 index 00000000..58da1ecd --- /dev/null +++ b/packages/pinball_flame/test/src/canvas/canvas_wrapper_test.dart @@ -0,0 +1,353 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/material.dart' hide Image; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/src/canvas/canvas_wrapper.dart'; + +class _MockCanvas extends Mock implements Canvas {} + +class _MockImage extends Mock implements Image {} + +class _MockPicture extends Mock implements Picture {} + +class _MockParagraph extends Mock implements Paragraph {} + +class _MockVertices extends Mock implements Vertices {} + +void main() { + group('CanvasWrapper', () { + group('CanvasWrapper', () { + late Canvas canvas; + late Path path; + late RRect rRect; + late Rect rect; + late Paint paint; + late Image atlas; + late BlendMode blendMode; + late Color color; + late Offset offset; + late Float64List float64list; + late Float32List float32list; + late Int32List int32list; + late Picture picture; + late Paragraph paragraph; + late Vertices vertices; + + setUp(() { + canvas = _MockCanvas(); + path = Path(); + rRect = RRect.zero; + rect = Rect.zero; + paint = Paint(); + atlas = _MockImage(); + blendMode = BlendMode.clear; + color = Colors.black; + offset = Offset.zero; + float64list = Float64List(1); + float32list = Float32List(1); + int32list = Int32List(1); + picture = _MockPicture(); + paragraph = _MockParagraph(); + vertices = _MockVertices(); + }); + + test("clipPath calls Canvas's clipPath", () { + CanvasWrapper() + ..canvas = canvas + ..clipPath(path, doAntiAlias: false); + verify( + () => canvas.clipPath(path, doAntiAlias: false), + ).called(1); + }); + + test("clipRRect calls Canvas's clipRRect", () { + CanvasWrapper() + ..canvas = canvas + ..clipRRect(rRect, doAntiAlias: false); + verify( + () => canvas.clipRRect(rRect, doAntiAlias: false), + ).called(1); + }); + + test("clipRect calls Canvas's clipRect", () { + CanvasWrapper() + ..canvas = canvas + ..clipRect(rect, doAntiAlias: false); + verify( + () => canvas.clipRect(rect, doAntiAlias: false), + ).called(1); + }); + + test("drawArc calls Canvas's drawArc", () { + CanvasWrapper() + ..canvas = canvas + ..drawArc(rect, 0, 1, false, paint); + verify( + () => canvas.drawArc(rect, 0, 1, false, paint), + ).called(1); + }); + + test("drawAtlas calls Canvas's drawAtlas", () { + CanvasWrapper() + ..canvas = canvas + ..drawAtlas(atlas, [], [], [], blendMode, rect, paint); + verify( + () => canvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint), + ).called(1); + }); + + test("drawCircle calls Canvas's drawCircle", () { + CanvasWrapper() + ..canvas = canvas + ..drawCircle(offset, 0, paint); + verify( + () => canvas.drawCircle(offset, 0, paint), + ).called(1); + }); + + test("drawColor calls Canvas's drawColor", () { + CanvasWrapper() + ..canvas = canvas + ..drawColor(color, blendMode); + verify( + () => canvas.drawColor(color, blendMode), + ).called(1); + }); + + test("drawDRRect calls Canvas's drawDRRect", () { + CanvasWrapper() + ..canvas = canvas + ..drawDRRect(rRect, rRect, paint); + verify( + () => canvas.drawDRRect(rRect, rRect, paint), + ).called(1); + }); + + test("drawImage calls Canvas's drawImage", () { + CanvasWrapper() + ..canvas = canvas + ..drawImage(atlas, offset, paint); + verify( + () => canvas.drawImage(atlas, offset, paint), + ).called(1); + }); + + test("drawImageNine calls Canvas's drawImageNine", () { + CanvasWrapper() + ..canvas = canvas + ..drawImageNine(atlas, rect, rect, paint); + verify( + () => canvas.drawImageNine(atlas, rect, rect, paint), + ).called(1); + }); + + test("drawImageRect calls Canvas's drawImageRect", () { + CanvasWrapper() + ..canvas = canvas + ..drawImageRect(atlas, rect, rect, paint); + verify( + () => canvas.drawImageRect(atlas, rect, rect, paint), + ).called(1); + }); + + test("drawLine calls Canvas's drawLine", () { + CanvasWrapper() + ..canvas = canvas + ..drawLine(offset, offset, paint); + verify( + () => canvas.drawLine(offset, offset, paint), + ).called(1); + }); + + test("drawOval calls Canvas's drawOval", () { + CanvasWrapper() + ..canvas = canvas + ..drawOval(rect, paint); + verify( + () => canvas.drawOval(rect, paint), + ).called(1); + }); + + test("drawPaint calls Canvas's drawPaint", () { + CanvasWrapper() + ..canvas = canvas + ..drawPaint(paint); + verify( + () => canvas.drawPaint(paint), + ).called(1); + }); + + test("drawParagraph calls Canvas's drawParagraph", () { + CanvasWrapper() + ..canvas = canvas + ..drawParagraph(paragraph, offset); + verify( + () => canvas.drawParagraph(paragraph, offset), + ).called(1); + }); + + test("drawPath calls Canvas's drawPath", () { + CanvasWrapper() + ..canvas = canvas + ..drawPath(path, paint); + verify( + () => canvas.drawPath(path, paint), + ).called(1); + }); + + test("drawPicture calls Canvas's drawPicture", () { + CanvasWrapper() + ..canvas = canvas + ..drawPicture(picture); + verify( + () => canvas.drawPicture(picture), + ).called(1); + }); + + test("drawPoints calls Canvas's drawPoints", () { + CanvasWrapper() + ..canvas = canvas + ..drawPoints(PointMode.points, [offset], paint); + verify( + () => canvas.drawPoints(PointMode.points, [offset], paint), + ).called(1); + }); + + test("drawRRect calls Canvas's drawRRect", () { + CanvasWrapper() + ..canvas = canvas + ..drawRRect(rRect, paint); + verify( + () => canvas.drawRRect(rRect, paint), + ).called(1); + }); + + test("drawRawAtlas calls Canvas's drawRawAtlas", () { + CanvasWrapper() + ..canvas = canvas + ..drawRawAtlas( + atlas, + float32list, + float32list, + int32list, + BlendMode.clear, + rect, + paint, + ); + verify( + () => canvas.drawRawAtlas( + atlas, + float32list, + float32list, + int32list, + BlendMode.clear, + rect, + paint, + ), + ).called(1); + }); + + test("drawRawPoints calls Canvas's drawRawPoints", () { + CanvasWrapper() + ..canvas = canvas + ..drawRawPoints(PointMode.points, float32list, paint); + verify( + () => canvas.drawRawPoints(PointMode.points, float32list, paint), + ).called(1); + }); + + test("drawRect calls Canvas's drawRect", () { + CanvasWrapper() + ..canvas = canvas + ..drawRect(rect, paint); + verify( + () => canvas.drawRect(rect, paint), + ).called(1); + }); + + test("drawShadow calls Canvas's drawShadow", () { + CanvasWrapper() + ..canvas = canvas + ..drawShadow(path, color, 0, false); + verify( + () => canvas.drawShadow(path, color, 0, false), + ).called(1); + }); + + test("drawVertices calls Canvas's drawVertices", () { + CanvasWrapper() + ..canvas = canvas + ..drawVertices(vertices, blendMode, paint); + verify( + () => canvas.drawVertices(vertices, blendMode, paint), + ).called(1); + }); + + test("getSaveCount calls Canvas's getSaveCount", () { + final canvasWrapper = CanvasWrapper()..canvas = canvas; + when(() => canvas.getSaveCount()).thenReturn(1); + canvasWrapper.getSaveCount(); + verify(() => canvas.getSaveCount()).called(1); + expect(canvasWrapper.getSaveCount(), 1); + }); + + test("restore calls Canvas's restore", () { + CanvasWrapper() + ..canvas = canvas + ..restore(); + verify(() => canvas.restore()).called(1); + }); + + test("rotate calls Canvas's rotate", () { + CanvasWrapper() + ..canvas = canvas + ..rotate(0); + verify(() => canvas.rotate(0)).called(1); + }); + + test("save calls Canvas's save", () { + CanvasWrapper() + ..canvas = canvas + ..save(); + verify(() => canvas.save()).called(1); + }); + + test("saveLayer calls Canvas's saveLayer", () { + CanvasWrapper() + ..canvas = canvas + ..saveLayer(rect, paint); + verify(() => canvas.saveLayer(rect, paint)).called(1); + }); + + test("scale calls Canvas's scale", () { + CanvasWrapper() + ..canvas = canvas + ..scale(0, 0); + verify(() => canvas.scale(0, 0)).called(1); + }); + + test("skew calls Canvas's skew", () { + CanvasWrapper() + ..canvas = canvas + ..skew(0, 0); + verify(() => canvas.skew(0, 0)).called(1); + }); + + test("transform calls Canvas's transform", () { + CanvasWrapper() + ..canvas = canvas + ..transform(float64list); + verify(() => canvas.transform(float64list)).called(1); + }); + + test("translate calls Canvas's translate", () { + CanvasWrapper() + ..canvas = canvas + ..translate(0, 0); + verify(() => canvas.translate(0, 0)).called(1); + }); + }); + }); +} diff --git a/packages/pinball_flame/test/src/canvas/z_canvas_component_test.dart b/packages/pinball_flame/test/src/canvas/z_canvas_component_test.dart new file mode 100644 index 00000000..67c45ec7 --- /dev/null +++ b/packages/pinball_flame/test/src/canvas/z_canvas_component_test.dart @@ -0,0 +1,80 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart' hide Image; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestCircleComponent extends CircleComponent with ZIndex { + _TestCircleComponent(Color color) + : super( + paint: Paint()..color = color, + radius: 10, + ); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('ZCanvasComponent', () { + final flameTester = FlameTester(FlameGame.new); + const goldensFilePath = '../goldens/rendering/'; + + test('can be instantiated', () { + expect( + ZCanvasComponent(), + isA(), + ); + }); + + flameTester.test('loads correctly', (game) async { + final component = ZCanvasComponent(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( + 'red circle renders behind blue circle', + setUp: (game, tester) async { + final canvas = ZCanvasComponent( + children: [ + _TestCircleComponent(Colors.blue)..zIndex = 1, + _TestCircleComponent(Colors.red)..zIndex = 0, + ], + ); + await game.ensureAdd(canvas); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldensFilePath}red_blue.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'blue circle renders behind red circle', + setUp: (game, tester) async { + final canvas = ZCanvasComponent( + children: [ + _TestCircleComponent(Colors.blue)..zIndex = 0, + _TestCircleComponent(Colors.red)..zIndex = 1 + ], + ); + await game.ensureAdd(canvas); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldensFilePath}blue_red.png'), + ); + }, + ); + }); +} diff --git a/packages/pinball_flame/test/src/goldens/rendering/blue_red.png b/packages/pinball_flame/test/src/goldens/rendering/blue_red.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca86375e8dffc98cb915c5a9eca4ecf94a8677c GIT binary patch literal 22364 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 X+Ve~i*X!6e;Kp4a`Fip_7@z(Vr{)6r literal 0 HcmV?d00001 diff --git a/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart b/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart deleted file mode 100644 index b6007bc5..00000000 --- a/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart +++ /dev/null @@ -1,385 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'dart:typed_data'; -import 'dart:ui'; - -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart' hide Image; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -class _TestCircleComponent extends CircleComponent with ZIndex { - _TestCircleComponent(Color color) - : super( - paint: Paint()..color = color, - radius: 10, - ); -} - -class _MockCanvas extends Mock implements Canvas {} - -class _MockImage extends Mock implements Image {} - -class _MockPicture extends Mock implements Picture {} - -class _MockParagraph extends Mock implements Paragraph {} - -class _MockVertices extends Mock implements Vertices {} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(FlameGame.new); - const goldenPrefix = 'golden/rendering/'; - - group('ZCanvasComponent', () { - flameTester.test('loads correctly', (game) async { - final component = ZCanvasComponent(); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); - }); - - flameTester.testGameWidget( - 'red circle renders behind blue circle', - setUp: (game, tester) async { - final canvas = ZCanvasComponent( - children: [ - _TestCircleComponent(Colors.blue)..zIndex = 1, - _TestCircleComponent(Colors.red)..zIndex = 0, - ], - ); - await game.ensureAdd(canvas); - - game.camera.followVector2(Vector2.zero()); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenPrefix}red_blue.png'), - ); - }, - ); - - flameTester.testGameWidget( - 'blue circle renders behind red circle', - setUp: (game, tester) async { - final canvas = ZCanvasComponent( - children: [ - _TestCircleComponent(Colors.blue)..zIndex = 0, - _TestCircleComponent(Colors.red)..zIndex = 1 - ], - ); - await game.ensureAdd(canvas); - - game.camera.followVector2(Vector2.zero()); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenPrefix}blue_red.png'), - ); - }, - ); - }); - - group('ZCanvas', () { - late Canvas canvas; - late Path path; - late RRect rRect; - late Rect rect; - late Paint paint; - late Image atlas; - late BlendMode blendMode; - late Color color; - late Offset offset; - late Float64List float64list; - late Float32List float32list; - late Int32List int32list; - late Picture picture; - late Paragraph paragraph; - late Vertices vertices; - - setUp(() { - canvas = _MockCanvas(); - path = Path(); - rRect = RRect.zero; - rect = Rect.zero; - paint = Paint(); - atlas = _MockImage(); - blendMode = BlendMode.clear; - color = Colors.black; - offset = Offset.zero; - float64list = Float64List(1); - float32list = Float32List(1); - int32list = Int32List(1); - picture = _MockPicture(); - paragraph = _MockParagraph(); - vertices = _MockVertices(); - }); - - test("clipPath calls Canvas's clipPath", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.clipPath(path, doAntiAlias: false); - verify( - () => canvas.clipPath(path, doAntiAlias: false), - ).called(1); - }); - - test("clipRRect calls Canvas's clipRRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.clipRRect(rRect, doAntiAlias: false); - verify( - () => canvas.clipRRect(rRect, doAntiAlias: false), - ).called(1); - }); - - test("clipRect calls Canvas's clipRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.clipRect(rect, doAntiAlias: false); - verify( - () => canvas.clipRect(rect, doAntiAlias: false), - ).called(1); - }); - - test("drawArc calls Canvas's drawArc", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawArc(rect, 0, 1, false, paint); - verify( - () => canvas.drawArc(rect, 0, 1, false, paint), - ).called(1); - }); - - test("drawAtlas calls Canvas's drawAtlas", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint); - verify( - () => canvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint), - ).called(1); - }); - - test("drawCircle calls Canvas's drawCircle", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawCircle(offset, 0, paint); - verify( - () => canvas.drawCircle(offset, 0, paint), - ).called(1); - }); - - test("drawColor calls Canvas's drawColor", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawColor(color, blendMode); - verify( - () => canvas.drawColor(color, blendMode), - ).called(1); - }); - - test("drawDRRect calls Canvas's drawDRRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawDRRect(rRect, rRect, paint); - verify( - () => canvas.drawDRRect(rRect, rRect, paint), - ).called(1); - }); - - test("drawImage calls Canvas's drawImage", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawImage(atlas, offset, paint); - verify( - () => canvas.drawImage(atlas, offset, paint), - ).called(1); - }); - - test("drawImageNine calls Canvas's drawImageNine", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawImageNine(atlas, rect, rect, paint); - verify( - () => canvas.drawImageNine(atlas, rect, rect, paint), - ).called(1); - }); - - test("drawImageRect calls Canvas's drawImageRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawImageRect(atlas, rect, rect, paint); - verify( - () => canvas.drawImageRect(atlas, rect, rect, paint), - ).called(1); - }); - - test("drawLine calls Canvas's drawLine", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawLine(offset, offset, paint); - verify( - () => canvas.drawLine(offset, offset, paint), - ).called(1); - }); - - test("drawOval calls Canvas's drawOval", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawOval(rect, paint); - verify( - () => canvas.drawOval(rect, paint), - ).called(1); - }); - - test("drawPaint calls Canvas's drawPaint", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawPaint(paint); - verify( - () => canvas.drawPaint(paint), - ).called(1); - }); - - test("drawParagraph calls Canvas's drawParagraph", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawParagraph(paragraph, offset); - verify( - () => canvas.drawParagraph(paragraph, offset), - ).called(1); - }); - - test("drawPath calls Canvas's drawPath", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawPath(path, paint); - verify( - () => canvas.drawPath(path, paint), - ).called(1); - }); - - test("drawPicture calls Canvas's drawPicture", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawPicture(picture); - verify( - () => canvas.drawPicture(picture), - ).called(1); - }); - - test("drawPoints calls Canvas's drawPoints", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawPoints(PointMode.points, [offset], paint); - verify( - () => canvas.drawPoints(PointMode.points, [offset], paint), - ).called(1); - }); - - test("drawRRect calls Canvas's drawRRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawRRect(rRect, paint); - verify( - () => canvas.drawRRect(rRect, paint), - ).called(1); - }); - - test("drawRawAtlas calls Canvas's drawRawAtlas", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawRawAtlas( - atlas, - float32list, - float32list, - int32list, - BlendMode.clear, - rect, - paint, - ); - verify( - () => canvas.drawRawAtlas( - atlas, - float32list, - float32list, - int32list, - BlendMode.clear, - rect, - paint, - ), - ).called(1); - }); - - test("drawRawPoints calls Canvas's drawRawPoints", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawRawPoints(PointMode.points, float32list, paint); - verify( - () => canvas.drawRawPoints(PointMode.points, float32list, paint), - ).called(1); - }); - - test("drawRect calls Canvas's drawRect", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawRect(rect, paint); - verify( - () => canvas.drawRect(rect, paint), - ).called(1); - }); - - test("drawShadow calls Canvas's drawShadow", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawShadow(path, color, 0, false); - verify( - () => canvas.drawShadow(path, color, 0, false), - ).called(1); - }); - - test("drawVertices calls Canvas's drawVertices", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.drawVertices(vertices, blendMode, paint); - verify( - () => canvas.drawVertices(vertices, blendMode, paint), - ).called(1); - }); - - test("getSaveCount calls Canvas's getSaveCount", () { - final zcanvas = ZCanvas()..canvas = canvas; - when(() => canvas.getSaveCount()).thenReturn(1); - zcanvas.getSaveCount(); - verify(() => canvas.getSaveCount()).called(1); - }); - - test("restore calls Canvas's restore", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.restore(); - verify(() => canvas.restore()).called(1); - }); - - test("rotate calls Canvas's rotate", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.rotate(0); - verify(() => canvas.rotate(0)).called(1); - }); - - test("save calls Canvas's save", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.save(); - verify(() => canvas.save()).called(1); - }); - - test("saveLayer calls Canvas's saveLayer", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.saveLayer(rect, paint); - verify(() => canvas.saveLayer(rect, paint)).called(1); - }); - - test("scale calls Canvas's scale", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.scale(0, 0); - verify(() => canvas.scale(0, 0)).called(1); - }); - - test("skew calls Canvas's skew", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.skew(0, 0); - verify(() => canvas.skew(0, 0)).called(1); - }); - - test("transform calls Canvas's transform", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.transform(float64list); - verify(() => canvas.transform(float64list)).called(1); - }); - - test("translate calls Canvas's translate", () { - final zcanvas = ZCanvas()..canvas = canvas; - zcanvas.translate(0, 0); - verify(() => canvas.translate(0, 0)).called(1); - }); - }); -} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index e1ed3084..f1f3a4cb 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,6 +1,9 @@ // ignore_for_file: cascade_invocations +import 'dart:ui'; + import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/gestures.dart'; @@ -233,14 +236,40 @@ void main() { }, ); - flameBlocTester.test( - 'one SkillShot', - (game) async { + flameBlocTester.test('one SkillShot', (game) async { + await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }); + + flameBlocTester.testGameWidget( + 'paints sprites with FilterQuality.medium', + setUp: (game, tester) async { + await game.images.loadAll(assets); await game.ready(); + + final descendants = game.descendants(); + final components = [ + ...descendants.whereType(), + ...descendants.whereType(), + ]; + expect(components, isNotEmpty); expect( - game.descendants().whereType().length, - equals(1), + components.whereType().length, + equals(components.length), ); + + await tester.pump(); + + for (final component in components) { + if (component is! HasPaint) return; + expect( + component.paint.filterQuality, + equals(FilterQuality.medium), + ); + } }, ); @@ -308,28 +337,25 @@ void main() { ); }); - group( - 'onNewState', - () { - flameTester.test( - 'spawns a ball', - (game) async { - final previousBalls = - game.descendants().whereType().toList(); - - game.controller.onNewState(_MockGameState()); - await game.ready(); - final currentBalls = - game.descendants().whereType().toList(); - - expect( - currentBalls.length, - equals(previousBalls.length + 1), - ); - }, - ); - }, - ); + group('onNewState', () { + flameTester.test( + 'spawns a ball', + (game) async { + final previousBalls = + game.descendants().whereType().toList(); + + game.controller.onNewState(_MockGameState()); + await game.ready(); + final currentBalls = + game.descendants().whereType().toList(); + + expect( + currentBalls.length, + equals(previousBalls.length + 1), + ); + }, + ); + }); }); });