Entities/TargetedParticle.cs

A Particle subclass that steers a non-colliding particle toward a target point or a target entity. It supports direct acceleration toward a moving anchor, optional settling at the target (stop when close), and an exponential lerp mode that moves the particle toward the target without using velocity.

Native Interop
namespace BlockParty;

/// <summary>
/// A <see cref="Particle"/> that steers toward a target point each step (port of the original
/// <c>TargetedParticle</c>). Used for Spikey's "spike send" burst: little squares stream out of
/// its third eye and accelerate onto the side it's grafting spikes to. Non-colliding.
///
/// When anchored to an entity the target tracks that entity (the offset is captured at spawn),
/// so the particles follow the rammed block as it drifts; with no anchor (arena walls) the
/// target is a fixed world point.
/// </summary>
public sealed class TargetedParticle : Particle
{
	private Entity2D _anchor;       // null => _targetOffset is an absolute world point
	private Vector2 _targetOffset;
	private float _targetAcceleration;
	private bool _settlesAtTarget;
	private bool _settled;
	private float _settleDistance;
	private bool _lerpsToTarget;
	private float _targetLerpSpeed;

	public void SetupTarget( Vector2 targetOffset, float targetAcceleration, Entity2D anchor )
	{
		_targetOffset = targetOffset;
		_targetAcceleration = targetAcceleration;
		_anchor = anchor;
		_collides = false;
	}

	/// <summary>Stop at the target instead of repeatedly overshooting it. Opt-in so the original
	/// Spikey and Sticky send particles retain their existing motion.
	///
	/// Also switches OFF wall/obstacle bouncing, because a settling particle is drawing a line to one
	/// specific point and must be free to reach it. Spikey/Sticky hop a few px to an adjacent block
	/// face so nothing is ever in the way, but the Slicer's charge stream crosses the WHOLE arena —
	/// and the beam it's charging ignores interior obstacles, so its particles have to as well.
	/// Left bouncing, a deflection flipped the particle away from its target, which the "passed it"
	/// test below then read as arrival: the stream visibly dead-ended on obstacle faces and arena
	/// walls instead of landing on the line.</summary>
	public void SettleAtTarget( float settleDistance = 2f )
	{
		_settlesAtTarget = true;
		_settleDistance = settleDistance;
		_bounces = false;
	}

	/// <summary>Move directly toward the live target with exponential interpolation instead of
	/// velocity or acceleration, so a short cosmetic convergence cannot overshoot its destination.</summary>
	public void LerpToTarget( float speed )
	{
		_lerpsToTarget = true;
		_targetLerpSpeed = speed;
		Velocity = Vector2.Zero;
		_bounces = false;
	}

	public override void Tick( float dt )
	{
		base.Tick( dt );

		Vector2 targetPos = (_anchor is not null) ? _anchor.Position + _targetOffset : _targetOffset;
		if ( _lerpsToTarget )
		{
			float amount = 1f - MathF.Exp( -_targetLerpSpeed * dt );
			Pos = Vector2.Lerp( Pos, targetPos, amount );
			if ( _clampsToArenaBounds ) ClampInsideArena();
			Velocity = Vector2.Zero;
			return;
		}

		if ( _settled )
		{
			Pos = targetPos;
			Velocity = Vector2.Zero;
			return;
		}

		Vector2 toTarget = targetPos - Position;
		float distance = toTarget.Length;
		float speedTowardTarget = distance > 0.001f ? Vector2.Dot( Velocity, toTarget / distance ) : 0f;
		// "Passed it" has to mean passed it CLOSELY: moving away from the target while still within the
		// last step's reach of it (if we really flew through it, we can't be further off than one step).
		// Ungated, ANY velocity reversal — a wall/obstacle bounce, a gravity turn — counted as arrival
		// and teleported the particle to its target from wherever it happened to be.
		float step = Velocity.Length * dt;
		bool passedTarget = speedTowardTarget < 0f && distance <= step + _settleDistance;
		if ( _settlesAtTarget && (distance <= _settleDistance || speedTowardTarget * dt >= distance || passedTarget) )
		{
			Pos = targetPos;
			Velocity = Vector2.Zero;
			_settled = true;
			return;
		}

		Velocity += Utils.Normalized( toTarget ) * _targetAcceleration * dt;
	}
}