mirror of https://github.com/flutter/pinball.git
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
parent
02edc75255
commit
5abfe6ab42
@ -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,
|
||||||
|
];
|
||||||
|
}
|
@ -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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
After Width: | Height: | Size: 154 KiB |
After Width: | Height: | Size: 126 KiB |
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…
Reference in new issue