diff --git a/lib/game/components/dino_desert.dart b/lib/game/components/dino_desert.dart index 799274f9..fc601791 100644 --- a/lib/game/components/dino_desert.dart +++ b/lib/game/components/dino_desert.dart @@ -1,4 +1,5 @@ import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -6,16 +7,32 @@ import 'package:pinball_components/pinball_components.dart'; /// Area located next to the [Launcher] containing the [ChromeDino] and /// [DinoWalls]. /// {@endtemplate} -// TODO(allisonryan0002): use a controller to initiate dino bonus when dino is -// fully implemented. class DinoDesert extends Component { /// {@macro dino_desert} DinoDesert() : super( children: [ - ChromeDino()..initialPosition = Vector2(12.3, -6.9), + ChromeDino( + children: [ + ScoringBehavior(points: 200000)..applyTo(['inside_mouth']), + ], + )..initialPosition = Vector2(12.6, -6.9), + _BarrierBehindDino(), DinoWalls(), Slingshots(), ], ); } + +class _BarrierBehindDino extends BodyComponent { + @override + Body createBody() { + final shape = EdgeShape() + ..set( + Vector2(25, -14.2), + Vector2(25, -7.7), + ); + + return world.createBody(BodyDef())..createFixtureFromShape(shape); + } +} diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index 63b6faf0..7469396a 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -68,7 +68,7 @@ class Ball extends BodyComponent /// /// If previously [stop]ped, the previous ball's velocity is not kept. void resume() { - body.gravityScale = Vector2(0, 1); + body.gravityScale = Vector2(1, 1); } /// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball]. diff --git a/packages/pinball_components/lib/src/components/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino.dart deleted file mode 100644 index 8619874e..00000000 --- a/packages/pinball_components/lib/src/components/chrome_dino.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'dart:async'; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart' hide Timer; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template chrome_dino} -/// Dino that swivels back and forth, opening its mouth to eat a [Ball]. -/// -/// Upon eating a [Ball], the dino rotates and spits the [Ball] out in a -/// different direction. -/// {@endtemplate} -class ChromeDino extends BodyComponent with InitialPosition, ZIndex { - /// {@macro chrome_dino} - ChromeDino() - : super( - renderBody: false, - ) { - zIndex = ZIndexes.dino; - } - - /// The size of the dinosaur mouth. - static final size = Vector2(5.5, 5); - - /// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc - /// motion. - Future<_ChromeDinoJoint> _anchorToJoint() async { - // TODO(allisonryan0002): try moving to anchor after new body is defined. - final anchor = _ChromeDinoAnchor() - ..initialPosition = initialPosition + Vector2(9, -4); - - await add(anchor); - - final jointDef = _ChromeDinoAnchorRevoluteJointDef( - chromeDino: this, - anchor: anchor, - ); - final joint = _ChromeDinoJoint(jointDef); - world.createJoint(joint); - - return joint; - } - - @override - Future onLoad() async { - await super.onLoad(); - final joint = await _anchorToJoint(); - const framesInAnimation = 98; - const animationFPS = 1 / 24; - await add( - TimerComponent( - period: (framesInAnimation / 2) * animationFPS, - onTick: joint._swivel, - repeat: true, - ), - ); - } - - List _createFixtureDefs() { - final fixtureDefs = []; - - // TODO(allisonryan0002): Update this shape to better match sprite. - final box = PolygonShape() - ..setAsBox( - size.x / 2, - size.y / 2, - initialPosition + Vector2(-4, 2), - -_ChromeDinoJoint._halfSweepingAngle, - ); - final fixtureDef = FixtureDef(box, density: 1); - fixtureDefs.add(fixtureDef); - - return fixtureDefs; - } - - @override - Body createBody() { - final bodyDef = BodyDef( - position: initialPosition, - type: BodyType.dynamic, - gravityScale: Vector2.zero(), - ); - - final body = world.createBody(bodyDef); - _createFixtureDefs().forEach(body.createFixture); - - return body; - } -} - -class _ChromeDinoAnchor extends JointAnchor { - _ChromeDinoAnchor(); - - // TODO(allisonryan0002): if these aren't moved when fixing the rendering, see - // if the joint can be created in onMount to resolve render syncing. - @override - Future onLoad() async { - await super.onLoad(); - await addAll([ - _ChromeDinoMouthSprite(), - _ChromeDinoHeadSprite(), - ]); - } -} - -/// {@template chrome_dino_anchor_revolute_joint_def} -/// Hinges a [ChromeDino] to a [_ChromeDinoAnchor]. -/// {@endtemplate} -class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef { - /// {@macro chrome_dino_anchor_revolute_joint_def} - _ChromeDinoAnchorRevoluteJointDef({ - required ChromeDino chromeDino, - required _ChromeDinoAnchor anchor, - }) { - initialize( - chromeDino.body, - anchor.body, - chromeDino.body.position + anchor.body.position, - ); - enableLimit = true; - lowerAngle = -_ChromeDinoJoint._halfSweepingAngle; - upperAngle = _ChromeDinoJoint._halfSweepingAngle; - - enableMotor = true; - maxMotorTorque = chromeDino.body.mass * 255; - motorSpeed = 2; - } -} - -class _ChromeDinoJoint extends RevoluteJoint { - _ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def); - - static const _halfSweepingAngle = 0.1143; - - /// Sweeps the [ChromeDino] up and down repeatedly. - void _swivel() { - setMotorSpeed(-motorSpeed); - } -} - -class _ChromeDinoMouthSprite extends SpriteAnimationComponent with HasGameRef { - _ChromeDinoMouthSprite() - : super( - anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), - angle: _ChromeDinoJoint._halfSweepingAngle, - ); - - @override - Future onLoad() async { - await super.onLoad(); - final image = gameRef.images.fromCache( - Assets.images.dino.animatronic.mouth.keyName, - ); - - const amountPerRow = 11; - const amountPerColumn = 9; - final textureSize = Vector2( - image.width / amountPerRow, - image.height / amountPerColumn, - ); - size = textureSize / 10; - - final data = SpriteAnimationData.sequenced( - amount: (amountPerColumn * amountPerRow) - 1, - amountPerRow: amountPerRow, - stepTime: 1 / 24, - textureSize: textureSize, - ); - animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; - } -} - -class _ChromeDinoHeadSprite extends SpriteAnimationComponent with HasGameRef { - _ChromeDinoHeadSprite() - : super( - anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), - angle: _ChromeDinoJoint._halfSweepingAngle, - ); - - @override - Future onLoad() async { - await super.onLoad(); - final image = gameRef.images.fromCache( - Assets.images.dino.animatronic.head.keyName, - ); - - const amountPerRow = 11; - const amountPerColumn = 9; - final textureSize = Vector2( - image.width / amountPerRow, - image.height / amountPerColumn, - ); - size = textureSize / 10; - - final data = SpriteAnimationData.sequenced( - amount: (amountPerColumn * amountPerRow) - 1, - amountPerRow: amountPerRow, - stepTime: 1 / 24, - textureSize: textureSize, - ); - animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; - } -} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/behaviors.dart new file mode 100644 index 00000000..3d4e5bad --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/behaviors.dart @@ -0,0 +1,4 @@ +export 'chrome_dino_chomping_behavior.dart'; +export 'chrome_dino_mouth_opening_behavior.dart'; +export 'chrome_dino_spitting_behavior.dart'; +export 'chrome_dino_swiveling_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart new file mode 100644 index 00000000..eff84ff4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart @@ -0,0 +1,20 @@ +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'; + +/// {@template chrome_dino_chomping_behavior} +/// Chomps a [Ball] after it has entered the [ChromeDino]'s mouth. +/// +/// The chomped [Ball] is hidden in the mouth until it is spit out. +/// {@endtemplate} +class ChromeDinoChompingBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + other.firstChild()!.setOpacity(0); + parent.bloc.onChomp(other); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior.dart new file mode 100644 index 00000000..6779a5d8 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior.dart @@ -0,0 +1,18 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template chrome_dino_mouth_opening_behavior} +/// Allows a [Ball] to enter the [ChromeDino] mouth when it is open. +/// {@endtemplate} +class ChromeDinoMouthOpeningBehavior extends ContactBehavior { + @override + void preSolve(Object other, Contact contact, Manifold oldManifold) { + super.preSolve(other, contact, oldManifold); + if (other is! Ball) return; + + if (parent.bloc.state.isMouthOpen && parent.firstChild() == null) { + contact.setEnabled(false); + } + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart new file mode 100644 index 00000000..78a8b9d5 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart @@ -0,0 +1,44 @@ +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'; + +/// {@template chrome_dino_spitting_behavior} +/// Spits the [Ball] from the [ChromeDino] the next time the mouth opens. +/// {@endtemplate} +class ChromeDinoSpittingBehavior extends Component + with ContactCallbacks, ParentIsA { + bool _waitingForSwivel = true; + + void _onNewState(ChromeDinoState state) { + if (state.status == ChromeDinoStatus.chomping) { + if (state.isMouthOpen && !_waitingForSwivel) { + add( + TimerComponent( + period: 0.4, + onTick: _spit, + removeOnFinish: true, + ), + ); + _waitingForSwivel = true; + } + if (_waitingForSwivel && !state.isMouthOpen) { + _waitingForSwivel = false; + } + } + } + + void _spit() { + parent.bloc.state.ball! + ..firstChild()!.setOpacity(1) + ..body.linearVelocity = Vector2(-50, 0); + parent.bloc.onSpit(); + } + + @override + Future onLoad() async { + await super.onLoad(); + + parent.bloc.stream.listen(_onNewState); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior.dart new file mode 100644 index 00000000..ab98c6a8 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior.dart @@ -0,0 +1,90 @@ +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'; + +/// {@template chrome_dino_swivel_behavior} +/// Swivels the [ChromeDino] up and down periodically to match its animation +/// sequence. +/// {@endtemplate} +class ChromeDinoSwivelingBehavior extends TimerComponent + with ParentIsA { + /// {@macro chrome_dino_swivel_behavior} + ChromeDinoSwivelingBehavior() + : super( + period: 98 / 48, + repeat: true, + ); + + late final RevoluteJoint _joint; + + @override + Future onLoad() async { + final anchor = _ChromeDinoAnchor() + ..initialPosition = parent.initialPosition + Vector2(9, -4); + await add(anchor); + + final jointDef = _ChromeDinoAnchorRevoluteJointDef( + chromeDino: parent, + anchor: anchor, + ); + _joint = RevoluteJoint(jointDef); + parent.world.createJoint(_joint); + } + + @override + void update(double dt) { + super.update(dt); + + final angle = _joint.jointAngle(); + + if (angle < _joint.upperLimit && + angle > _joint.lowerLimit && + parent.bloc.state.isMouthOpen) { + parent.bloc.onCloseMouth(); + } else if ((angle >= _joint.upperLimit || angle <= _joint.lowerLimit) && + !parent.bloc.state.isMouthOpen) { + parent.bloc.onOpenMouth(); + } + } + + @override + void onTick() { + super.onTick(); + _joint.setMotorSpeed(-_joint.motorSpeed); + } +} + +class _ChromeDinoAnchor extends JointAnchor + with ParentIsA { + @override + void onMount() { + super.onMount(); + parent.parent.children + .whereType() + .forEach((sprite) { + sprite.animation!.currentIndex = 45; + sprite.changeParent(this); + }); + } +} + +class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef { + _ChromeDinoAnchorRevoluteJointDef({ + required ChromeDino chromeDino, + required _ChromeDinoAnchor anchor, + }) { + initialize( + chromeDino.body, + anchor.body, + chromeDino.body.position + anchor.body.position, + ); + enableLimit = true; + lowerAngle = -ChromeDino.halfSweepingAngle; + upperAngle = ChromeDino.halfSweepingAngle; + + enableMotor = true; + maxMotorTorque = chromeDino.body.mass * 255; + motorSpeed = 2; + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino/chrome_dino.dart new file mode 100644 index 00000000..38a335b9 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/chrome_dino.dart @@ -0,0 +1,207 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Timer; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/chrome_dino_cubit.dart'; + +/// {@template chrome_dino} +/// Dino that swivels back and forth, opening its mouth to eat a [Ball]. +/// +/// Upon eating a [Ball], the dino rotates and spits the [Ball] out in the +/// opposite direction. +/// {@endtemplate} +class ChromeDino extends BodyComponent + with InitialPosition, ContactCallbacks, ZIndex { + /// {@macro chrome_dino} + ChromeDino({Iterable? children}) + : bloc = ChromeDinoCubit(), + super( + children: [ + _ChromeDinoMouthSprite(), + _ChromeDinoHeadSprite(), + ChromeDinoMouthOpeningBehavior()..applyTo(['mouth_opening']), + ChromeDinoSwivelingBehavior(), + ChromeDinoChompingBehavior()..applyTo(['inside_mouth']), + ChromeDinoSpittingBehavior(), + ...?children, + ], + renderBody: false, + ) { + zIndex = ZIndexes.dino; + } + + /// Creates a [ChromeDino] without any children. + /// + /// This can be used for testing [ChromeDino]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + ChromeDino.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 ChromeDinoCubit bloc; + + /// Angle to rotate the dino up or down from the starting horizontal position. + static const halfSweepingAngle = 0.1143; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + List _createFixtureDefs() { + const mouthAngle = -(halfSweepingAngle + 0.28); + final size = Vector2(5.5, 6); + + final topEdge = PolygonShape() + ..setAsBox( + size.x / 2, + 0.1, + initialPosition + Vector2(-4.2, -1.4), + mouthAngle, + ); + final topEdgeFixtureDef = FixtureDef(topEdge, density: 100); + + final backEdge = PolygonShape() + ..setAsBox( + 0.1, + size.y / 2, + initialPosition + Vector2(-1.3, 0.5), + -halfSweepingAngle, + ); + final backEdgeFixtureDef = FixtureDef(backEdge, density: 100); + + final bottomEdge = PolygonShape() + ..setAsBox( + size.x / 2, + 0.1, + initialPosition + Vector2(-3.5, 4.7), + mouthAngle, + ); + final bottomEdgeFixtureDef = FixtureDef( + bottomEdge, + density: 100, + ); + + final mouthOpeningEdge = PolygonShape() + ..setAsBox( + 0.1, + size.y / 2.5, + initialPosition + Vector2(-6.4, 2.7), + -halfSweepingAngle, + ); + final mouthOpeningEdgeFixtureDef = FixtureDef( + mouthOpeningEdge, + density: 0.1, + userData: 'mouth_opening', + ); + + final insideSensor = PolygonShape() + ..setAsBox( + 0.2, + 0.2, + initialPosition + Vector2(-3.5, 1.5), + 0, + ); + final insideSensorFixtureDef = FixtureDef( + insideSensor, + isSensor: true, + userData: 'inside_mouth', + ); + + return [ + topEdgeFixtureDef, + backEdgeFixtureDef, + bottomEdgeFixtureDef, + mouthOpeningEdgeFixtureDef, + insideSensorFixtureDef, + ]; + } + + @override + Body createBody() { + final bodyDef = BodyDef( + position: initialPosition, + type: BodyType.dynamic, + gravityScale: Vector2.zero(), + ); + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} + +class _ChromeDinoMouthSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoMouthSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: ChromeDino.halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.mouth.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data); + } +} + +class _ChromeDinoHeadSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoHeadSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: ChromeDino.halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.head.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data); + } +} 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 new file mode 100644 index 00000000..b98a4093 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart @@ -0,0 +1,27 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball_components/pinball_components.dart'; + +part 'chrome_dino_state.dart'; + +class ChromeDinoCubit extends Cubit { + ChromeDinoCubit() : super(const ChromeDinoState.inital()); + + void onOpenMouth() { + emit(state.copyWith(isMouthOpen: true)); + } + + void onCloseMouth() { + emit(state.copyWith(isMouthOpen: false)); + } + + void onChomp(Ball ball) { + emit(state.copyWith(status: ChromeDinoStatus.chomping, ball: ball)); + } + + void onSpit() { + emit(state.copyWith(status: ChromeDinoStatus.idle)); + } +} 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 new file mode 100644 index 00000000..a5d3b183 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart @@ -0,0 +1,46 @@ +// ignore_for_file: public_member_api_docs + +part of 'chrome_dino_cubit.dart'; + +enum ChromeDinoStatus { + idle, + chomping, +} + +class ChromeDinoState extends Equatable { + const ChromeDinoState({ + required this.status, + required this.isMouthOpen, + this.ball, + }); + + const ChromeDinoState.inital() + : this( + status: ChromeDinoStatus.idle, + isMouthOpen: false, + ); + + final ChromeDinoStatus status; + final bool isMouthOpen; + final Ball? ball; + + ChromeDinoState copyWith({ + ChromeDinoStatus? status, + bool? isMouthOpen, + Ball? ball, + }) { + final state = ChromeDinoState( + status: status ?? this.status, + isMouthOpen: isMouthOpen ?? this.isMouthOpen, + ball: ball ?? this.ball, + ); + return state; + } + + @override + List get props => [ + status, + isMouthOpen, + ball, + ]; +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index bb676e1b..394f32ed 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -8,7 +8,7 @@ export 'board_dimensions.dart'; export 'board_side.dart'; export 'boundaries.dart'; export 'camera_zoom.dart'; -export 'chrome_dino.dart'; +export 'chrome_dino/chrome_dino.dart'; export 'dash_animatronic.dart'; export 'dash_nest_bumper/dash_nest_bumper.dart'; export 'dino_walls.dart'; diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index 33c5670d..ab867e3b 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -26,3 +26,5 @@ class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} class MockMultiplierCubit extends Mock implements MultiplierCubit {} + +class MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} 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 new file mode 100644 index 00000000..141d89de --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart @@ -0,0 +1,61 @@ +// 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/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 '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'ChromeDinoChompingBehavior', + () { + test('can be instantiated', () { + expect( + ChromeDinoChompingBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact sets ball sprite to be invisible and calls onChomp', + (game) async { + final ball = Ball(baseColor: Colors.red); + final behavior = ChromeDinoChompingBehavior(); + final bloc = MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: true, + ), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAddAll([chromeDino, ball]); + + final contact = MockContact(); + final fixture = MockFixture(); + when(() => contact.fixtureA).thenReturn(fixture); + when(() => fixture.userData).thenReturn('inside_mouth'); + + behavior.beginContact(ball, contact); + + expect(ball.firstChild()!.getOpacity(), isZero); + + verify(() => bloc.onChomp(ball)).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior_test.dart new file mode 100644 index 00000000..31e49cd4 --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior_test.dart @@ -0,0 +1,58 @@ +// 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/chrome_dino/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'ChromeDinoMouthOpeningBehavior', + () { + test('can be instantiated', () { + expect( + ChromeDinoMouthOpeningBehavior(), + isA(), + ); + }); + + flameTester.test( + 'preSolve disables contact when the mouth is open ' + 'and there is not ball in the mouth', + (game) async { + final behavior = ChromeDinoMouthOpeningBehavior(); + final bloc = MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: true, + ), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final contact = MockContact(); + final fixture = MockFixture(); + when(() => contact.fixtureA).thenReturn(fixture); + when(() => fixture.userData).thenReturn('mouth_opening'); + + behavior.preSolve(MockBall(), contact, Manifold()); + + verify(() => contact.setEnabled(false)).called(1); + }, + ); + }, + ); +} 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 new file mode 100644 index 00000000..9993fa16 --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart @@ -0,0 +1,108 @@ +// ignore_for_file: cascade_invocations + +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 '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'ChromeDinoSpittingBehavior', + () { + test('can be instantiated', () { + expect( + ChromeDinoSpittingBehavior(), + isA(), + ); + }); + + group('on the next time the mouth opens and status is chomping', () { + flameTester.test( + 'sets ball sprite to visible and sets a linear velocity', + (game) async { + final ball = Ball(baseColor: Colors.red); + final behavior = ChromeDinoSpittingBehavior(); + final bloc = MockChromeDinoCubit(); + final streamController = StreamController(); + final chompingState = ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ball: ball, + ); + whenListen( + bloc, + streamController.stream, + initialState: chompingState, + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAddAll([chromeDino, ball]); + + streamController.add(chompingState.copyWith(isMouthOpen: false)); + streamController.add(chompingState.copyWith(isMouthOpen: true)); + await game.ready(); + + game + .descendants() + .whereType() + .single + .timer + .onTick!(); + + expect(ball.firstChild()!.getOpacity(), equals(1)); + expect(ball.body.linearVelocity, equals(Vector2(-50, 0))); + }, + ); + + flameTester.test( + 'calls onSpit', + (game) async { + final ball = Ball(baseColor: Colors.red); + final behavior = ChromeDinoSpittingBehavior(); + final bloc = MockChromeDinoCubit(); + final streamController = StreamController(); + final chompingState = ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ball: ball, + ); + whenListen( + bloc, + streamController.stream, + initialState: chompingState, + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAddAll([chromeDino, ball]); + + streamController.add(chompingState.copyWith(isMouthOpen: false)); + streamController.add(chompingState.copyWith(isMouthOpen: true)); + await game.ready(); + + game + .descendants() + .whereType() + .single + .timer + .onTick!(); + + verify(bloc.onSpit).called(1); + }, + ); + }); + }, + ); +} 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 new file mode 100644 index 00000000..5dd6c06d --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart @@ -0,0 +1,169 @@ +// 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/chrome_dino/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'ChromeDinoSwivelingBehavior', + () { + const swivelPeriod = 98 / 48; + + test('can be instantiated', () { + expect( + ChromeDinoSwivelingBehavior(), + isA(), + ); + }); + + flameTester.test( + 'creates a RevoluteJoint', + (game) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState.inital(), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + expect( + game.world.joints.whereType().single, + isNotNull, + ); + }, + ); + + flameTester.test( + 'reverses swivel direction on each timer tick', + (game) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState.inital(), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final timer = behavior.timer; + final joint = game.world.joints.whereType().single; + + expect(joint.motorSpeed, isPositive); + + timer.onTick!(); + game.update(0); + expect(joint.motorSpeed, isNegative); + + timer.onTick!(); + game.update(0); + expect(joint.motorSpeed, isPositive); + }, + ); + + group('calls', () { + flameTester.testGameWidget( + 'onCloseMouth when joint angle is between limits ' + 'and mouth is open', + setUp: (game, tester) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: + const ChromeDinoState.inital().copyWith(isMouthOpen: true), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final joint = game.world.joints.whereType().single; + final angle = joint.jointAngle(); + expect( + angle < joint.upperLimit && angle > joint.lowerLimit, + isTrue, + ); + game.update(0); + + verify(bloc.onCloseMouth).called(1); + }, + ); + + flameTester.testGameWidget( + 'onOpenMouth when joint angle is greater than the upperLimit ' + 'and mouth is closed', + setUp: (game, tester) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: + const ChromeDinoState.inital().copyWith(isMouthOpen: false), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final joint = game.world.joints.whereType().single; + + game.update(swivelPeriod / 2); + await tester.pump(); + final angle = joint.jointAngle(); + expect(angle >= joint.upperLimit, isTrue); + + verify(bloc.onOpenMouth).called(1); + }, + ); + + flameTester.testGameWidget( + 'onOpenMouth when joint angle is less than the lowerLimit ' + 'and mouth is closed', + setUp: (game, tester) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: + const ChromeDinoState.inital().copyWith(isMouthOpen: false), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final joint = game.world.joints.whereType().single; + + game.update(swivelPeriod * 1.5); + await tester.pump(); + final angle = joint.jointAngle(); + expect(angle <= joint.lowerLimit, isTrue); + + verify(bloc.onOpenMouth).called(1); + }, + ); + }); + }, + ); +} 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 new file mode 100644 index 00000000..9826cf95 --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart @@ -0,0 +1,141 @@ +// 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/chrome_dino/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.animatronic.head.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('ChromeDino', () { + flameTester.test( + 'loads correctly', + (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + + expect(game.contains(chromeDino), isTrue); + }, + ); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.ensureAdd(ChromeDino()); + game.camera.followVector2(Vector2.zero()); + await tester.pump(); + }, + verify: (game, tester) async { + final swivelAnimationDuration = game + .descendants() + .whereType() + .first + .animation! + .totalDuration() / + 2; + game.update(swivelAnimationDuration); + await tester.pump(); + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/down.png'), + ); + + game.update(swivelAnimationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/middle.png'), + ); + + game.update(swivelAnimationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/up.png'), + ); + }, + ); + + // 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 = MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState.inital(), + ); + when(bloc.close).thenAnswer((_) async {}); + final chromeDino = ChromeDino.test(bloc: bloc); + + await game.ensureAdd(chromeDino); + game.remove(chromeDino); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('a ChromeDinoMouthOpeningBehavior', (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + expect( + chromeDino.children + .whereType() + .single, + isNotNull, + ); + }); + + flameTester.test('a ChromeDinoSwivelingBehavior', (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + expect( + chromeDino.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('a ChromeDinoChompingBehavior', (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + expect( + chromeDino.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('a ChromeDinoSpittingBehavior', (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + expect( + chromeDino.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('new children', (game) async { + final component = Component(); + final chromeDino = ChromeDino( + children: [component], + ); + await game.ensureAdd(chromeDino); + expect(chromeDino.children, contains(component)); + }); + }); + }); +} 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 new file mode 100644 index 00000000..30e67511 --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart @@ -0,0 +1,71 @@ +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'; + +void main() { + group( + 'ChromeDinoCubit', + () { + final ball = Ball(baseColor: Colors.red); + + blocTest( + 'onOpenMouth emits true', + build: ChromeDinoCubit.new, + act: (bloc) => bloc.onOpenMouth(), + expect: () => [ + isA().having( + (state) => state.isMouthOpen, + 'isMouthOpen', + true, + ) + ], + ); + + blocTest( + 'onCloseMouth emits false', + build: ChromeDinoCubit.new, + act: (bloc) => bloc.onCloseMouth(), + expect: () => [ + isA().having( + (state) => state.isMouthOpen, + 'isMouthOpen', + false, + ) + ], + ); + + blocTest( + 'onChomp emits ChromeDinoStatus.chomping and chomped ball', + build: ChromeDinoCubit.new, + act: (bloc) => bloc.onChomp(ball), + expect: () => [ + isA() + ..having( + (state) => state.status, + 'status', + ChromeDinoStatus.chomping, + ) + ..having( + (state) => state.ball, + 'ball', + ball, + ) + ], + ); + + blocTest( + 'onSpit emits ChromeDinoStatus.idle', + build: ChromeDinoCubit.new, + act: (bloc) => bloc.onSpit(), + expect: () => [ + isA().having( + (state) => state.status, + 'status', + ChromeDinoStatus.idle, + ) + ], + ); + }, + ); +} 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 new file mode 100644 index 00000000..d067674b --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart @@ -0,0 +1,88 @@ +// 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'; + +void main() { + group('ChromeDinoState', () { + test('supports value equality', () { + expect( + ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ), + equals( + const ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + const ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ), + isNotNull, + ); + }); + + test('initial is idle with mouth closed', () { + const initialState = ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: false, + ); + expect(ChromeDinoState.inital(), equals(initialState)); + }); + }); + + group('copyWith', () { + test( + 'copies correctly ' + 'when no argument specified', + () { + const chromeDinoState = ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ); + expect( + chromeDinoState.copyWith(), + equals(chromeDinoState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + final ball = Ball(baseColor: Colors.red); + const chromeDinoState = ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ); + final otherChromeDinoState = ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: false, + ball: ball, + ); + expect(chromeDinoState, isNot(equals(otherChromeDinoState))); + + expect( + chromeDinoState.copyWith( + status: ChromeDinoStatus.idle, + isMouthOpen: false, + ball: ball, + ), + equals(otherChromeDinoState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/down.png b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/down.png new file mode 100644 index 00000000..eaeb458e Binary files /dev/null and b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/down.png differ diff --git a/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/middle.png b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/middle.png new file mode 100644 index 00000000..d8665644 Binary files /dev/null and b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/middle.png differ diff --git a/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/up.png b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/up.png new file mode 100644 index 00000000..a584b785 Binary files /dev/null and b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/up.png differ diff --git a/packages/pinball_components/test/src/components/chrome_dino_test.dart b/packages/pinball_components/test/src/components/chrome_dino_test.dart deleted file mode 100644 index f97270b9..00000000 --- a/packages/pinball_components/test/src/components/chrome_dino_test.dart +++ /dev/null @@ -1,109 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame/components.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.dino.animatronic.mouth.keyName, - Assets.images.dino.animatronic.head.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); - - group('ChromeDino', () { - flameTester.test( - 'loads correctly', - (game) async { - final chromeDino = ChromeDino(); - await game.ensureAdd(chromeDino); - - expect(game.contains(chromeDino), isTrue); - }, - ); - - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - await game.images.loadAll(assets); - await game.ensureAdd(ChromeDino()); - game.camera.followVector2(Vector2.zero()); - await tester.pump(); - }, - verify: (game, tester) async { - final sweepAnimationDuration = game - .descendants() - .whereType() - .first - .animation! - .totalDuration() / - 2; - - await expectLater( - find.byGame(), - matchesGoldenFile('golden/chrome_dino/up.png'), - ); - - game.update(sweepAnimationDuration * 0.25); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/chrome_dino/middle.png'), - ); - - game.update(sweepAnimationDuration * 0.25); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/chrome_dino/down.png'), - ); - }, - ); - - group('swivels', () { - flameTester.test( - 'up', - (game) async { - final chromeDino = ChromeDino(); - await game.ensureAdd(chromeDino); - game.camera.followVector2(Vector2.zero()); - - final sweepAnimationDuration = game - .descendants() - .whereType() - .first - .animation! - .totalDuration() / - 2; - game.update(sweepAnimationDuration * 1.5); - - expect(chromeDino.body.angularVelocity, isPositive); - }, - ); - - flameTester.test( - 'down', - (game) async { - final chromeDino = ChromeDino(); - await game.ensureAdd(chromeDino); - game.camera.followVector2(Vector2.zero()); - - final sweepAnimationDuration = game - .descendants() - .whereType() - .first - .animation! - .totalDuration() / - 2; - game.update(sweepAnimationDuration * 0.5); - - expect(chromeDino.body.angularVelocity, isNegative); - }, - ); - }); - }); -} diff --git a/test/game/components/dino_desert_test.dart b/test/game/components/dino_desert_test.dart new file mode 100644 index 00000000..35a2d25b --- /dev/null +++ b/test/game/components/dino_desert_test.dart @@ -0,0 +1,79 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.dino.animatronic.head.keyName, + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + ]; + + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('DinoDesert', () { + flameTester.test('loads correctly', (game) async { + final component = DinoDesert(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + group('loads', () { + flameTester.test( + 'a ChromeDino', + (game) async { + await game.ensureAdd(DinoDesert()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'DinoWalls', + (game) async { + await game.ensureAdd(DinoDesert()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameTester.test( + 'Slingshots', + (game) async { + await game.ensureAdd(DinoDesert()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + }); + + flameTester.test( + 'adds ScoringBehavior to ChromeDino', + (game) async { + await game.ensureAdd(DinoDesert()); + + final chromeDino = game.descendants().whereType().single; + expect( + chromeDino.firstChild(), + isNotNull, + ); + }, + ); + }); +}