Entities/Blocks/BlockSpikey.cs
namespace BlockParty;

/// <summary>
/// Spikey block — on any resolved impact it grafts deadly spikes onto the facing side. Phase 1:
/// only onto a block it rams; phase 2: onto a block, or (when it rams a bare wall) onto the
/// ARENA wall it hits. It opens a "third eye" in its forehead and fires a burst of little
/// targeted particles that stream onto the side before the spikes appear there.
/// Port of the gameplay core of the original BlockSpikey.
/// </summary>
public sealed class BlockSpikey : Block
{
	const float BLOCK_SPIKE_TIME = 6f;
	const float ARENA_SPIKE_TIME = 4f;
	const float THIRD_EYE_HIDE = 0.2f;

	// Forehead offset the spike-send particles fire from (centre + up a little).
	static readonly Vector2 ThirdEyeOffset = new( 0, 12f );

	SpriteRenderer _thirdEye;
	float _thirdEyeTimer = -1f;   // counts down the hide animation; -1 = idle
	bool _thirdEyeHiding;

	// Pending graft — applied once the particle burst has had time to land. Exactly one of _grBlock
	// (a block's side) / _grFace (an arena wall or obstacle face) is set.
	bool _spikesPending;
	protected override bool CanActivateTimedAbility => !_spikesPending;
	Block _grBlock;               // target block; null => a wall face
	Direction _grSide;            // block side (block target only)
	WallFace _grFace;             // arena wall / obstacle face target
	float _grTime;
	float _applyTimer;

	// Particle burst emission.
	int _emitRemaining;
	float _emitInterval;
	float _emitTimer;
	Vector2 _emitStart;
	Line _emitLine;
	Entity2D _emitAnchor;         // block (target tracks it) or null (arena wall)

	public override void CreateVisuals()
	{
		base.CreateVisuals();
		_thirdEye = SpriteLayer.Add( GameObject, SpritePath, new Vector2( 40, 40 ), "third_eye_appear", childOrder: 5 );
		_thirdEye.Enabled = false;
	}

	public override void Impact( Block otherBlock )
	{
		ImpactEffects( otherBlock );
		if ( IsDead ) return;

		if ( Phase == 1 || Phase == 2 )
		{
			if ( otherBlock is not null )
			{
				// Spike the side of the block we rammed (its side facing us) — unless it already has
				// spikes there (live, growing in, or retracting). Don't even open the third eye / fire
				// the particle burst for a graft that would be a no-op.
				var side = Globals.GetOppositeDirection( MoveDirection );
				if ( !otherBlock.HasSpikes( side ) )
					BeginSpikeSend( otherBlock, side, null, BLOCK_SPIKE_TIME, GetSideLineForBlock( side, otherBlock ),
						otherBlock, Rng.Int( 8, 12 ), Rng.Float( 0.1f, 0.2f ), 0.6f );
			}
			else if ( Phase == 2 )
			{
				// Phase 2 only: ramming a bare solid grafts spikes onto the face we hit — an interior
				// obstacle's face if that's what we rammed, or the arena wall on this side when nothing
				// else was hit. Both queries are position-aware: a split side grafts only the segment
				// we're against. Any other non-block solid (the hardened Twin statue) has no face to
				// graft onto — never fall through to the arena wall from a mid-arena impact.
				// Skip entirely (no third eye / particles) if the face is already spiked — a permanent
				// level spike, or a temporary graft still live/switching there.
				var faceNormal = Globals.GetOppositeDirection( MoveDirection );
				WallFace face = LastImpactOther switch
				{
					Obstacle ob => Stage.FindObstacleFace( ob, faceNormal, Position ),
					null => Stage.FindArenaFace( MoveDirection, Position ),   // the boundary segment we rammed
					_ => null,
				};
				if ( face is not null && !face.SpikesPresent )
					BeginSpikeSend( null, Direction.None, face, ARENA_SPIKE_TIME, face.Segment,
						null, Rng.Int( 15, 22 ), Rng.Float( 0.1f, 0.2f ), 0.8f );
			}
		}

		CloseEyesAndPickNewDirection();
	}

	void BeginSpikeSend( Block block, Direction side, WallFace face, float spikeTime, Line targetLine, Entity2D anchor, int numParticles, float emitTime, float applyDelay )
	{
		// Land any still-pending graft before starting a new one (impacts shouldn't overlap, but be safe).
		FlushPendingSpikes();

		_spikesPending = true;
		_grBlock = block;
		_grSide = side;
		_grFace = face;
		_grTime = spikeTime;
		_applyTimer = applyDelay;

		_emitStart = Position + ThirdEyeOffset;
		_emitLine = targetLine;
		_emitAnchor = anchor;
		_emitRemaining = numParticles;
		_emitInterval = (numParticles > 0) ? emitTime / numParticles : emitTime;
		_emitTimer = 0f;          // fire the first one immediately

		ShowThirdEye();
		Audio.PlaySfx( SfxType.SpikesSend, Position );
	}

	void EmitSpikeParticle()
	{
		Vector2 pointOnLine = _emitLine.A + Utils.Normalized( _emitLine.B - _emitLine.A ) * Rng.Float( 0f, _emitLine.Length );
		Vector2 targetOffset = (_emitAnchor is not null) ? pointOnLine - _emitAnchor.Position : pointOnLine;
		var kind = (Rng.Float( 0f, 1f ) < 0.25f) ? ParticleKind.AddSpikes1 : ParticleKind.AddSpikes0;

		Stage.AddTargetedParticle( _emitStart, Vector2.Zero, 0.90f, 0f, kind,
			Rng.Float( 1.2f, 1.85f ), Rng.Int( 3, 5 ), targetOffset, Rng.Float( 1400f, 1700f ), _emitAnchor );
	}

	/// <summary>The graft target got mimic-swapped mid-send: carry the pending spikes (and the particle
	/// anchor) to the disguise, so they land on the block actually standing there.</summary>
	public override void OnBlockReplaced( Block old, Block replacement )
	{
		if ( ReferenceEquals( _grBlock, old ) ) _grBlock = replacement;
		if ( ReferenceEquals( _emitAnchor, old ) ) _emitAnchor = replacement;
	}

	void FlushPendingSpikes()
	{
		if ( !_spikesPending ) return;

		if ( _grBlock is not null && !_grBlock.Replaced )
			_grBlock.AddSpikesToSide( _grSide, _grTime );
		else if ( _grFace is not null )
			Stage.AddSpikesToFace( _grFace, _grTime );

		_spikesPending = false;
		_grBlock = null;
		_grFace = null;
		HideThirdEye();
	}

	void ShowThirdEye()
	{
		_thirdEye.Enabled = true;
		_thirdEye.PlayAnimation( "third_eye_appear" );
		_thirdEyeHiding = false;
		_thirdEyeTimer = -1f;             // stays open until the graft lands
	}

	void HideThirdEye()
	{
		if ( _thirdEye is null || !_thirdEye.Enabled ) return;
		_thirdEye.PlayAnimation( "third_eye_disappear" );
		_thirdEyeHiding = true;
		_thirdEyeTimer = THIRD_EYE_HIDE;
	}

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

		// Stream out the spike-send particles.
		if ( _emitRemaining > 0 )
		{
			_emitTimer -= dt;
			while ( _emitRemaining > 0 && _emitTimer <= 0f )
			{
				EmitSpikeParticle();
				_emitRemaining--;
				_emitTimer += _emitInterval;
			}
		}

		// Graft the spikes once the burst has had time to land.
		if ( _spikesPending )
		{
			_applyTimer -= dt;
			if ( _applyTimer <= 0f )
				FlushPendingSpikes();
		}

		// Third-eye hide animation.
		if ( _thirdEyeHiding && _thirdEyeTimer > 0f )
		{
			_thirdEyeTimer -= dt;
			if ( _thirdEyeTimer <= 0f )
			{
				_thirdEye.Enabled = false;
				_thirdEyeHiding = false;
				_thirdEyeTimer = -1f;
			}
		}
	}

	static Line GetSideLineForBlock( Direction dir, Block b ) => dir switch
	{
		Direction.Left => new Line( new Vector2( b.Left, b.Bottom ), new Vector2( b.Left, b.Top ) ),
		Direction.Right => new Line( new Vector2( b.Right, b.Bottom ), new Vector2( b.Right, b.Top ) ),
		Direction.Down => new Line( new Vector2( b.Left, b.Bottom ), new Vector2( b.Right, b.Bottom ) ),
		_ => new Line( new Vector2( b.Left, b.Top ), new Vector2( b.Right, b.Top ) ),
	};
}