feat: add `ChromeDino` behaviors (#277)

* feat: included new head and mouth assets

* feat: including sprites

* feat: sized sprites

* feat: included new sprites

* feat: adjusted SpriteAnimationComponent

* feat: adjusted tracing logic

* feat: added Traceable to ChromeDinoGame

* feat: synced dino animation

* refactor: fix crazy rendering

* test: chrome dino

* refactor: dino sandbox

* chore: revert spaceship changes

* chore: move assets for sanbox game

* refactor: move dino walls and bottom boundary

* refactor: move dino for moved dino wall

* test: update goldens

* feat: add behaviors to dino

* test: dino behaviors

* feat: add invisible barrier behind dino

* chore: update boundaries golden

* chore: update dino goldens

* fix: spitting test

* fix: two coverage lines

* fix: unused import

* chore: fix personal nits

* fix: test description error

* chore: dino zIndex

* fix: dino desert test from merge

* refactor: moved Vector2 size

* refactor: removed unused userData

* feat: mvoed ChromeDino slightly back

Co-authored-by: alestiago <dev@alestiago.com>
pull/284/head
Allison Ryan 2 years ago committed by GitHub
parent 02edc75255
commit 5abfe6ab42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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);
}
}

@ -68,7 +68,7 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
///
/// 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].

@ -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<void> 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<FixtureDef> _createFixtureDefs() {
final fixtureDefs = <FixtureDef>[];
// 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<void> 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<void> 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<void> 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;
}
}

@ -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';

@ -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<ChromeDino> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
other.firstChild<SpriteComponent>()!.setOpacity(0);
parent.bloc.onChomp(other);
}
}

@ -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<ChromeDino> {
@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<Ball>() == null) {
contact.setEnabled(false);
}
}
}

@ -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<ChromeDino> {
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<SpriteComponent>()!.setOpacity(1)
..body.linearVelocity = Vector2(-50, 0);
parent.bloc.onSpit();
}
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen(_onNewState);
}
}

@ -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<ChromeDino> {
/// {@macro chrome_dino_swivel_behavior}
ChromeDinoSwivelingBehavior()
: super(
period: 98 / 48,
repeat: true,
);
late final RevoluteJoint _joint;
@override
Future<void> 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<ChromeDinoSwivelingBehavior> {
@override
void onMount() {
super.onMount();
parent.parent.children
.whereType<SpriteAnimationComponent>()
.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;
}
}

@ -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<Component>? 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<FixtureDef> _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<void> 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<void> 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);
}
}

@ -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<ChromeDinoState> {
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));
}
}

@ -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<Object?> get props => [
status,
isMouthOpen,
ball,
];
}

@ -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';

@ -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 {}

@ -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<ChromeDinoChompingBehavior>(),
);
});
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<ChromeDinoState>.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<SpriteComponent>()!.getOpacity(), isZero);
verify(() => bloc.onChomp(ball)).called(1);
},
);
},
);
}

@ -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<ChromeDinoMouthOpeningBehavior>(),
);
});
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<ChromeDinoState>.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);
},
);
},
);
}

@ -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<ChromeDinoSpittingBehavior>(),
);
});
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<ChromeDinoState>();
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<TimerComponent>()
.single
.timer
.onTick!();
expect(ball.firstChild<SpriteComponent>()!.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<ChromeDinoState>();
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<TimerComponent>()
.single
.timer
.onTick!();
verify(bloc.onSpit).called(1);
},
);
});
},
);
}

@ -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<ChromeDinoSwivelingBehavior>(),
);
});
flameTester.test(
'creates a RevoluteJoint',
(game) async {
final behavior = ChromeDinoSwivelingBehavior();
final bloc = MockChromeDinoCubit();
whenListen(
bloc,
const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(),
);
final chromeDino = ChromeDino.test(bloc: bloc);
await chromeDino.add(behavior);
await game.ensureAdd(chromeDino);
expect(
game.world.joints.whereType<RevoluteJoint>().single,
isNotNull,
);
},
);
flameTester.test(
'reverses swivel direction on each timer tick',
(game) async {
final behavior = ChromeDinoSwivelingBehavior();
final bloc = MockChromeDinoCubit();
whenListen(
bloc,
const Stream<ChromeDinoState>.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<RevoluteJoint>().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<ChromeDinoState>.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<RevoluteJoint>().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<ChromeDinoState>.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<RevoluteJoint>().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<ChromeDinoState>.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<RevoluteJoint>().single;
game.update(swivelPeriod * 1.5);
await tester.pump();
final angle = joint.jointAngle();
expect(angle <= joint.lowerLimit, isTrue);
verify(bloc.onOpenMouth).called(1);
},
);
});
},
);
}

@ -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<SpriteAnimationComponent>()
.first
.animation!
.totalDuration() /
2;
game.update(swivelAnimationDuration);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/chrome_dino/down.png'),
);
game.update(swivelAnimationDuration * 0.25);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/chrome_dino/middle.png'),
);
game.update(swivelAnimationDuration * 0.25);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
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<ChromeDinoState>.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<ChromeDinoMouthOpeningBehavior>()
.single,
isNotNull,
);
});
flameTester.test('a ChromeDinoSwivelingBehavior', (game) async {
final chromeDino = ChromeDino();
await game.ensureAdd(chromeDino);
expect(
chromeDino.children.whereType<ChromeDinoSwivelingBehavior>().single,
isNotNull,
);
});
flameTester.test('a ChromeDinoChompingBehavior', (game) async {
final chromeDino = ChromeDino();
await game.ensureAdd(chromeDino);
expect(
chromeDino.children.whereType<ChromeDinoChompingBehavior>().single,
isNotNull,
);
});
flameTester.test('a ChromeDinoSpittingBehavior', (game) async {
final chromeDino = ChromeDino();
await game.ensureAdd(chromeDino);
expect(
chromeDino.children.whereType<ChromeDinoSpittingBehavior>().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));
});
});
});
}

@ -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<ChromeDinoCubit, ChromeDinoState>(
'onOpenMouth emits true',
build: ChromeDinoCubit.new,
act: (bloc) => bloc.onOpenMouth(),
expect: () => [
isA<ChromeDinoState>().having(
(state) => state.isMouthOpen,
'isMouthOpen',
true,
)
],
);
blocTest<ChromeDinoCubit, ChromeDinoState>(
'onCloseMouth emits false',
build: ChromeDinoCubit.new,
act: (bloc) => bloc.onCloseMouth(),
expect: () => [
isA<ChromeDinoState>().having(
(state) => state.isMouthOpen,
'isMouthOpen',
false,
)
],
);
blocTest<ChromeDinoCubit, ChromeDinoState>(
'onChomp emits ChromeDinoStatus.chomping and chomped ball',
build: ChromeDinoCubit.new,
act: (bloc) => bloc.onChomp(ball),
expect: () => [
isA<ChromeDinoState>()
..having(
(state) => state.status,
'status',
ChromeDinoStatus.chomping,
)
..having(
(state) => state.ball,
'ball',
ball,
)
],
);
blocTest<ChromeDinoCubit, ChromeDinoState>(
'onSpit emits ChromeDinoStatus.idle',
build: ChromeDinoCubit.new,
act: (bloc) => bloc.onSpit(),
expect: () => [
isA<ChromeDinoState>().having(
(state) => state.status,
'status',
ChromeDinoStatus.idle,
)
],
);
},
);
}

@ -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),
);
},
);
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

@ -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<SpriteAnimationComponent>()
.first
.animation!
.totalDuration() /
2;
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/chrome_dino/up.png'),
);
game.update(sweepAnimationDuration * 0.25);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/chrome_dino/middle.png'),
);
game.update(sweepAnimationDuration * 0.25);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
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<SpriteAnimationComponent>()
.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<SpriteAnimationComponent>()
.first
.animation!
.totalDuration() /
2;
game.update(sweepAnimationDuration * 0.5);
expect(chromeDino.body.angularVelocity, isNegative);
},
);
});
});
}

@ -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<ChromeDino>().length,
equals(1),
);
},
);
flameTester.test(
'DinoWalls',
(game) async {
await game.ensureAdd(DinoDesert());
expect(
game.descendants().whereType<DinoWalls>().length,
equals(1),
);
},
);
flameTester.test(
'Slingshots',
(game) async {
await game.ensureAdd(DinoDesert());
expect(
game.descendants().whereType<Slingshots>().length,
equals(1),
);
},
);
});
flameTester.test(
'adds ScoringBehavior to ChromeDino',
(game) async {
await game.ensureAdd(DinoDesert());
final chromeDino = game.descendants().whereType<ChromeDino>().single;
expect(
chromeDino.firstChild<ScoringBehavior>(),
isNotNull,
);
},
);
});
}
Loading…
Cancel
Save