diff --git a/lib/game/components/dino_desert.dart b/lib/game/components/dino_desert.dart index 9e912575..c4646ea9 100644 --- a/lib/game/components/dino_desert.dart +++ b/lib/game/components/dino_desert.dart @@ -7,14 +7,16 @@ import 'package:pinball_flame/pinball_flame.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 Blueprint { /// {@macro dino_desert} DinoDesert() : super( components: [ - ChromeDino()..initialPosition = Vector2(12.3, -6.9), + ChromeDino( + children: [ + ScoringBehavior(points: 200000)..applyTo(['inside_mouth']), + ], + )..initialPosition = Vector2(12.3, -6.9), ], blueprints: [ DinoWalls(), diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index 64c7d884..6edcce33 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -67,7 +67,7 @@ class Ball extends BodyComponent /// /// If previously [stop]ped, the previous ball's velocity is not kept. void resume() { - body.gravityScale = Vector2(0, 1); + body.gravityScale = Vector2(1, 1); } /// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball]. diff --git a/packages/pinball_components/lib/src/components/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino.dart deleted file mode 100644 index e1a1a1fc..00000000 --- a/packages/pinball_components/lib/src/components/chrome_dino.dart +++ /dev/null @@ -1,202 +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'; - -/// {@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 { - /// {@macro chrome_dino} - ChromeDino() - : super( - priority: RenderPriority.dino, - renderBody: false, - ); - - /// The size of the dinosaur mouth. - static final size = Vector2(5.5, 5); - - /// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc - /// motion. - Future<_ChromeDinoJoint> _anchorToJoint() async { - // TODO(allisonryan0002): try moving to anchor after new body is defined. - final anchor = _ChromeDinoAnchor() - ..initialPosition = initialPosition + Vector2(9, -4); - - await add(anchor); - - final jointDef = _ChromeDinoAnchorRevoluteJointDef( - chromeDino: this, - anchor: anchor, - ); - final joint = _ChromeDinoJoint(jointDef); - world.createJoint(joint); - - return joint; - } - - @override - Future onLoad() async { - await super.onLoad(); - final joint = await _anchorToJoint(); - const framesInAnimation = 98; - const animationFPS = 1 / 24; - await add( - TimerComponent( - period: (framesInAnimation / 2) * animationFPS, - onTick: joint._swivel, - repeat: true, - ), - ); - } - - List _createFixtureDefs() { - final fixtureDefs = []; - - // TODO(allisonryan0002): Update this shape to better match sprite. - final box = PolygonShape() - ..setAsBox( - size.x / 2, - size.y / 2, - initialPosition + Vector2(-4, 2), - -_ChromeDinoJoint._halfSweepingAngle, - ); - final fixtureDef = FixtureDef(box, density: 1); - fixtureDefs.add(fixtureDef); - - return fixtureDefs; - } - - @override - Body createBody() { - final bodyDef = BodyDef( - position: initialPosition, - type: BodyType.dynamic, - gravityScale: Vector2.zero(), - ); - - final body = world.createBody(bodyDef); - _createFixtureDefs().forEach(body.createFixture); - - return body; - } -} - -class _ChromeDinoAnchor extends JointAnchor { - _ChromeDinoAnchor(); - - // TODO(allisonryan0002): if these aren't moved when fixing the rendering, see - // if the joint can be created in onMount to resolve render syncing. - @override - Future onLoad() async { - await super.onLoad(); - await addAll([ - _ChromeDinoMouthSprite(), - _ChromeDinoHeadSprite(), - ]); - } -} - -/// {@template chrome_dino_anchor_revolute_joint_def} -/// Hinges a [ChromeDino] to a [_ChromeDinoAnchor]. -/// {@endtemplate} -class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef { - /// {@macro chrome_dino_anchor_revolute_joint_def} - _ChromeDinoAnchorRevoluteJointDef({ - required ChromeDino chromeDino, - required _ChromeDinoAnchor anchor, - }) { - initialize( - chromeDino.body, - anchor.body, - chromeDino.body.position + anchor.body.position, - ); - enableLimit = true; - lowerAngle = -_ChromeDinoJoint._halfSweepingAngle; - upperAngle = _ChromeDinoJoint._halfSweepingAngle; - - enableMotor = true; - maxMotorTorque = chromeDino.body.mass * 255; - motorSpeed = 2; - } -} - -class _ChromeDinoJoint extends RevoluteJoint { - _ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def); - - static const _halfSweepingAngle = 0.1143; - - /// Sweeps the [ChromeDino] up and down repeatedly. - void _swivel() { - setMotorSpeed(-motorSpeed); - } -} - -class _ChromeDinoMouthSprite extends SpriteAnimationComponent with HasGameRef { - _ChromeDinoMouthSprite() - : super( - anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), - angle: _ChromeDinoJoint._halfSweepingAngle, - ); - - @override - Future onLoad() async { - await super.onLoad(); - final image = gameRef.images.fromCache( - Assets.images.dino.animatronic.mouth.keyName, - ); - - const amountPerRow = 11; - const amountPerColumn = 9; - final textureSize = Vector2( - image.width / amountPerRow, - image.height / amountPerColumn, - ); - size = textureSize / 10; - - final data = SpriteAnimationData.sequenced( - amount: (amountPerColumn * amountPerRow) - 1, - amountPerRow: amountPerRow, - stepTime: 1 / 24, - textureSize: textureSize, - ); - animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; - } -} - -class _ChromeDinoHeadSprite extends SpriteAnimationComponent with HasGameRef { - _ChromeDinoHeadSprite() - : super( - anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), - angle: _ChromeDinoJoint._halfSweepingAngle, - ); - - @override - Future onLoad() async { - await super.onLoad(); - final image = gameRef.images.fromCache( - Assets.images.dino.animatronic.head.keyName, - ); - - const amountPerRow = 11; - const amountPerColumn = 9; - final textureSize = Vector2( - image.width / amountPerRow, - image.height / amountPerColumn, - ); - size = textureSize / 10; - - final data = SpriteAnimationData.sequenced( - amount: (amountPerColumn * amountPerRow) - 1, - amountPerRow: amountPerRow, - stepTime: 1 / 24, - textureSize: textureSize, - ); - animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; - } -} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/behaviors.dart new file mode 100644 index 00000000..3d4e5bad --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/behaviors.dart @@ -0,0 +1,4 @@ +export 'chrome_dino_chomping_behavior.dart'; +export 'chrome_dino_mouth_opening_behavior.dart'; +export 'chrome_dino_spitting_behavior.dart'; +export 'chrome_dino_swiveling_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart new file mode 100644 index 00000000..eff84ff4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart @@ -0,0 +1,20 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template chrome_dino_chomping_behavior} +/// Chomps a [Ball] after it has entered the [ChromeDino]'s mouth. +/// +/// The chomped [Ball] is hidden in the mouth until it is spit out. +/// {@endtemplate} +class ChromeDinoChompingBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + other.firstChild()!.setOpacity(0); + parent.bloc.onChomp(other); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior.dart new file mode 100644 index 00000000..6779a5d8 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior.dart @@ -0,0 +1,18 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template chrome_dino_mouth_opening_behavior} +/// Allows a [Ball] to enter the [ChromeDino] mouth when it is open. +/// {@endtemplate} +class ChromeDinoMouthOpeningBehavior extends ContactBehavior { + @override + void preSolve(Object other, Contact contact, Manifold oldManifold) { + super.preSolve(other, contact, oldManifold); + if (other is! Ball) return; + + if (parent.bloc.state.isMouthOpen && parent.firstChild() == null) { + contact.setEnabled(false); + } + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart new file mode 100644 index 00000000..68dd9c44 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart @@ -0,0 +1,47 @@ +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 { + /// {@macro chrome_dino_spitting_behavior} + ChromeDinoSpittingBehavior(); + + bool _waitingForSwivel = true; + + void _onNewState(ChromeDinoState state) { + if (state.status == ChromeDinoStatus.chomping) { + if (state.isMouthOpen && !_waitingForSwivel) { + add( + TimerComponent( + period: 0.4, + onTick: _spit, + removeOnFinish: true, + ), + ); + _waitingForSwivel = true; + } + if (_waitingForSwivel && !state.isMouthOpen) { + _waitingForSwivel = false; + } + } + } + + void _spit() { + parent.bloc.state.ball! + ..firstChild()!.setOpacity(1) + ..body.linearVelocity = Vector2(-50, 3); + parent.bloc.onSpit(); + } + + @override + Future onLoad() async { + await super.onLoad(); + + parent.bloc.stream.listen(_onNewState); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior.dart new file mode 100644 index 00000000..ab98c6a8 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior.dart @@ -0,0 +1,90 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template chrome_dino_swivel_behavior} +/// Swivels the [ChromeDino] up and down periodically to match its animation +/// sequence. +/// {@endtemplate} +class ChromeDinoSwivelingBehavior extends TimerComponent + with ParentIsA { + /// {@macro chrome_dino_swivel_behavior} + ChromeDinoSwivelingBehavior() + : super( + period: 98 / 48, + repeat: true, + ); + + late final RevoluteJoint _joint; + + @override + Future onLoad() async { + final anchor = _ChromeDinoAnchor() + ..initialPosition = parent.initialPosition + Vector2(9, -4); + await add(anchor); + + final jointDef = _ChromeDinoAnchorRevoluteJointDef( + chromeDino: parent, + anchor: anchor, + ); + _joint = RevoluteJoint(jointDef); + parent.world.createJoint(_joint); + } + + @override + void update(double dt) { + super.update(dt); + + final angle = _joint.jointAngle(); + + if (angle < _joint.upperLimit && + angle > _joint.lowerLimit && + parent.bloc.state.isMouthOpen) { + parent.bloc.onCloseMouth(); + } else if ((angle >= _joint.upperLimit || angle <= _joint.lowerLimit) && + !parent.bloc.state.isMouthOpen) { + parent.bloc.onOpenMouth(); + } + } + + @override + void onTick() { + super.onTick(); + _joint.setMotorSpeed(-_joint.motorSpeed); + } +} + +class _ChromeDinoAnchor extends JointAnchor + with ParentIsA { + @override + void onMount() { + super.onMount(); + parent.parent.children + .whereType() + .forEach((sprite) { + sprite.animation!.currentIndex = 45; + sprite.changeParent(this); + }); + } +} + +class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef { + _ChromeDinoAnchorRevoluteJointDef({ + required ChromeDino chromeDino, + required _ChromeDinoAnchor anchor, + }) { + initialize( + chromeDino.body, + anchor.body, + chromeDino.body.position + anchor.body.position, + ); + enableLimit = true; + lowerAngle = -ChromeDino.halfSweepingAngle; + upperAngle = ChromeDino.halfSweepingAngle; + + enableMotor = true; + maxMotorTorque = chromeDino.body.mass * 255; + motorSpeed = 2; + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino/chrome_dino.dart new file mode 100644 index 00000000..9944094b --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/chrome_dino.dart @@ -0,0 +1,208 @@ +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'; + +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 { + /// {@macro chrome_dino} + ChromeDino({Iterable? children}) + : bloc = ChromeDinoCubit(), + super( + priority: RenderPriority.dino, + children: [ + _ChromeDinoMouthSprite(), + _ChromeDinoHeadSprite(), + ChromeDinoMouthOpeningBehavior()..applyTo(['mouth_opening']), + ChromeDinoSwivelingBehavior(), + ChromeDinoChompingBehavior()..applyTo(['inside_mouth']), + ChromeDinoSpittingBehavior(), + ...?children, + ], + renderBody: false, + ); + + /// 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; + + /// The size of the dinosaur mouth. + static final size = Vector2(5.5, 6); + + /// Angle to rotate the dino up or down from the starting horizontal position. + static const halfSweepingAngle = 0.1143; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + List _createFixtureDefs() { + const mouthAngle = -(halfSweepingAngle + 0.28); + + final 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( + userData: this, + position: initialPosition, + type: BodyType.dynamic, + gravityScale: Vector2.zero(), + ); + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} + +class _ChromeDinoMouthSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoMouthSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: ChromeDino.halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.mouth.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data); + } +} + +class _ChromeDinoHeadSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoHeadSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: ChromeDino.halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.head.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart new file mode 100644 index 00000000..b98a4093 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart @@ -0,0 +1,27 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball_components/pinball_components.dart'; + +part 'chrome_dino_state.dart'; + +class ChromeDinoCubit extends Cubit { + ChromeDinoCubit() : super(const ChromeDinoState.inital()); + + void onOpenMouth() { + emit(state.copyWith(isMouthOpen: true)); + } + + void onCloseMouth() { + emit(state.copyWith(isMouthOpen: false)); + } + + void onChomp(Ball ball) { + emit(state.copyWith(status: ChromeDinoStatus.chomping, ball: ball)); + } + + void onSpit() { + emit(state.copyWith(status: ChromeDinoStatus.idle)); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart new file mode 100644 index 00000000..a5d3b183 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart @@ -0,0 +1,46 @@ +// ignore_for_file: public_member_api_docs + +part of 'chrome_dino_cubit.dart'; + +enum ChromeDinoStatus { + idle, + chomping, +} + +class ChromeDinoState extends Equatable { + const ChromeDinoState({ + required this.status, + required this.isMouthOpen, + this.ball, + }); + + const ChromeDinoState.inital() + : this( + status: ChromeDinoStatus.idle, + isMouthOpen: false, + ); + + final ChromeDinoStatus status; + final bool isMouthOpen; + final Ball? ball; + + ChromeDinoState copyWith({ + ChromeDinoStatus? status, + bool? isMouthOpen, + Ball? ball, + }) { + final state = ChromeDinoState( + status: status ?? this.status, + isMouthOpen: isMouthOpen ?? this.isMouthOpen, + ball: ball ?? this.ball, + ); + return state; + } + + @override + List get props => [ + status, + isMouthOpen, + ball, + ]; +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 2781030e..4c60c5be 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -8,7 +8,7 @@ export 'board_dimensions.dart'; export 'board_side.dart'; export 'boundaries.dart'; export 'camera_zoom.dart'; -export 'chrome_dino.dart'; +export 'chrome_dino/chrome_dino.dart'; export 'dash_animatronic.dart'; export 'dash_nest_bumper/dash_nest_bumper.dart'; export 'dino_walls.dart';