Entities/Blocks/BlockSticky.cs
namespace BlockParty;

/// <summary>
/// Sticky block — a pink, gooey block that traps the player against whichever face they touch.
///
/// The stick MECHANIC itself lives in the player (see <c>Player.HandleStickyBlocks</c>), keyed off
/// this subtype exactly like the spike/laser interactions key off <see cref="BlockSpikey"/> /
/// <see cref="BlockSquid"/> — the block only advertises "I am sticky right now" via
/// <see cref="IsStickyActive"/>. Keeping the behaviour player-side lets it hook straight into the
/// player's collision flags, glue and jump/gravity gates without threading forces through a channel
/// (unlike Wind/Magnet, which are pure forces and so live in the block).
///
	/// While active the player, on first contact with a face, is glued flush to it and can't jump off (a
	/// refused jump plays a gooey "nope" sfx). Every sticky face lets them travel along it at reduced
	/// speed and escape over an edge. A hard SLAM by the block frees the player for a short
/// window (flinging them off with the block's momentum). The block is sticky only at phase 1+ (like
/// Wind/Magnet gate on phase), so at phase 0 it's an ordinary block whose sides you press to advance it.
///
/// Visual tells (this class): pink goo dribbles off the block once sticky (see <see cref="EmitDrip"/>),
/// and a translucent lumpy-pink goo overlay sits over each side's button (see <see cref="CreateVisuals"/>),
/// its opacity breathing with the phase (see <see cref="UpdateGoo"/>).
/// </summary>
public sealed class BlockSticky : Block
{
	/// <summary>Whether the block is currently trapping-capable: alive and advanced past phase 0.</summary>
	public bool IsStickyActive => !IsDead && Phase >= 1;
	public override int StickyPhase( Direction dir )
		=> Math.Max( IsStickyActive ? Phase : 0, base.StickyPhase( dir ) );

	const float TRANSFER_STICKY_TIME = 8f;
	const float TRANSFER_APPLY_DELAY = 0.6f;
	const float TRANSFER_EMIT_TIME = 0.18f;
	const int TRANSFER_PARTICLE_COUNT = 10;

	Block _stickyTarget;
	Direction _stickyTargetSide;
	float _stickyApplyTimer;
	int _stickyEmitRemaining;
	float _stickyEmitTimer;
	Vector2 _stickyEmitStart;
	Line _stickyTargetLine;

	// Translucent lumpy-pink goo overlay per side (baked sprites/blocks/sticky_goo.sprite). Appears with
	// the sticky effect (phase 1+). Its opacity BREATHES — oscillates between a dim and a bright alpha —
	// as a tell: a noticeable pulse at phase 1, and a much stronger/faster pulse at phase 2 (both faster
	// pulsing AND a wider opacity swing). Cosmetic (a time accumulator on a visual-only alpha; not sim state).
	readonly Dictionary<Direction, SpriteRenderer> _goo = new();
	const float GOO_FADE_SPEED = 30.0f;     // per-second lerp toward the target (high, so the fast pulse tracks)
	// Per-phase pulse: (dim alpha, bright alpha, rad/s). Phase 2 is far more dramatic.
	const float GOO_DIM_1 = 0.28f, GOO_BRIGHT_1 = 0.68f, GOO_PULSE_SPEED_1 = 8.0f;
	const float GOO_DIM_2 = 0.10f, GOO_BRIGHT_2 = 0.85f, GOO_PULSE_SPEED_2 = 16.0f;
	float _gooPulse;

	// Gooey-drip tell: once sticky (phase 1+) the block dribbles pink goo so it reads as sticky at a
	// glance, and the effect escalates at phase 2 (a faster ooze). The drips are purely cosmetic —
	// spawned through GameStage.AddWindParticle (non-colliding) using the COSMETIC Rng — so emitting
	// them never advances the authoritative sim Rng (same discipline as the Wind/Magnet streams).
	//
	// Emission edge is chosen so the goo is ALWAYS visible: a drip renders behind the block, so while the
	// block MOVES it emits off its TRAILING edge (opposite MoveDirection) — the block slides away from
	// those drips, leaving a goo trail in open space no matter which way it's headed (the old
	// bottom-only version vanished whenever the block outran its own falling drips). While slow/stopped
	// it dribbles off a random edge and the drips fall under gravity.
	const float DRIP_RATE_PHASE_1 = 6.0f;    // drips/sec
	const float DRIP_RATE_PHASE_2 = 15.0f;
	const float DRIP_MOVING_SPEED = 20.0f;   // above this the block trails goo off its back edge
	const float DRIP_GRAVITY = Globals.GRAVITY_STR_BLOOD; // negative -> the goo falls downward
	float _dripAccum;

	public override void CreateVisuals()
	{
		base.CreateVisuals();

		// One goo overlay per side, above the buttons (childOrder 5, over face/mouth/eyes/eyebrows/sides).
		// Translucent (Opaque off + AlphaCutoff 0, like the wind/magnet lane overlays) so the button reads
		// through it and the runtime alpha tint gives true smooth transparency. Starts fully transparent;
		// UpdateGoo fades it in once the block is sticky.
		foreach ( var dir in Globals.GetAllDirections() )
		{
			var sr = SpriteLayer.Add( GameObject, "sprites/blocks/sticky_goo.sprite",
				new Vector2( 40, 40 ), $"goo_{Globals.GetStringForDirection( dir )}", childOrder: 5 );
			sr.Opaque = false;
			sr.AlphaCutoff = 0f;
			sr.Color = new Color( 1f, 1f, 1f, 0f );
			_goo[dir] = sr;
		}
	}

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

		if ( Phase >= 2 && otherBlock is not null )
		{
			Direction side = Globals.GetOppositeDirection( MoveDirection );
			if ( otherBlock.StickyPhase( side ) == 0 )
				BeginStickySend( otherBlock, side );
		}

		CloseEyesAndPickNewDirection();
	}

	void BeginStickySend( Block target, Direction side )
	{
		FlushPendingSticky();

		_stickyTarget = target;
		_stickyTargetSide = side;
		_stickyApplyTimer = TRANSFER_APPLY_DELAY;
		_stickyEmitRemaining = TRANSFER_PARTICLE_COUNT;
		_stickyEmitTimer = 0f;
		_stickyEmitStart = Position + Globals.GetVectorForDirection( MoveDirection ) * 12f;
		_stickyTargetLine = GetSideLineForBlock( side, target );
	}

	void EmitStickySendParticle()
	{
		Vector2 along = Utils.Normalized( _stickyTargetLine.B - _stickyTargetLine.A );
		Vector2 pointOnLine = _stickyTargetLine.A + along * Rng.CosmeticFloat( 0f, _stickyTargetLine.Length );
		Vector2 targetOffset = pointOnLine - _stickyTarget.Position;
		ParticleKind kind = Rng.CosmeticValue() < 0.25f ? ParticleKind.StickyGoo1 : ParticleKind.StickyGoo0;

		Stage.AddTargetedParticle( _stickyEmitStart, Vector2.Zero, 0.9f, 0f, kind,
			Rng.CosmeticFloat( 1.2f, 1.85f ), Rng.CosmeticInt( 3, 5 ), targetOffset,
			Rng.CosmeticFloat( 1400f, 1700f ), _stickyTarget );
	}

	/// <summary>The transfer target got mimic-swapped mid-send: carry the pending goo to the disguise
	/// (same spot/side), so the graft lands on the block actually standing there.</summary>
	public override void OnBlockReplaced( Block old, Block replacement )
	{
		if ( !ReferenceEquals( _stickyTarget, old ) ) return;
		_stickyTarget = replacement;
		_stickyTargetLine = GetSideLineForBlock( _stickyTargetSide, replacement );
	}

	void FlushPendingSticky()
	{
		if ( _stickyTarget is null ) return;

		if ( !_stickyTarget.Replaced )
		{
			_stickyTarget.AddStickyToSide( _stickyTargetSide, TRANSFER_STICKY_TIME );
			Audio.PlaySfx( SfxType.StickyStick, _stickyTarget.Position );
		}

		_stickyTarget = null;
		_stickyTargetSide = Direction.None;
	}

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

		if ( _stickyEmitRemaining > 0 && _stickyTarget is not null && !_stickyTarget.Replaced )
		{
			_stickyEmitTimer -= dt;
			while ( _stickyEmitRemaining > 0 && _stickyEmitTimer <= 0f )
			{
				EmitStickySendParticle();
				_stickyEmitRemaining--;
				_stickyEmitTimer += TRANSFER_EMIT_TIME / TRANSFER_PARTICLE_COUNT;
			}
		}

		if ( _stickyTarget is not null )
		{
			_stickyApplyTimer -= dt;
			if ( _stickyApplyTimer <= 0f )
				FlushPendingSticky();
		}

		UpdateGoo( dt );

		if ( IsDead || Phase < 1 ) return;

		_dripAccum += (Phase >= 2 ? DRIP_RATE_PHASE_2 : DRIP_RATE_PHASE_1) * dt;
		while ( _dripAccum >= 1f )
		{
			_dripAccum -= 1f;
			EmitDrip();
		}
	}

	/// <summary>Ease each side's goo overlay toward the breathing opacity (0 when not sticky). The pulse
	/// swings wider and faster at phase 2 than phase 1.</summary>
	void UpdateGoo( float dt )
	{
		bool sticky = IsStickyActive;
		float step = Math.Clamp( dt * GOO_FADE_SPEED, 0f, 1f );

		bool p2 = Phase >= 2;
		_gooPulse += dt * (p2 ? GOO_PULSE_SPEED_2 : GOO_PULSE_SPEED_1);
		float osc = 0.5f + 0.5f * MathF.Sin( _gooPulse );  // 0..1
		float dim = p2 ? GOO_DIM_2 : GOO_DIM_1;
		float bright = p2 ? GOO_BRIGHT_2 : GOO_BRIGHT_1;
		float target = sticky ? dim + (bright - dim) * osc : 0f;

		foreach ( var dir in Globals.GetAllDirections() )
		{
			var sr = _goo[dir];
			if ( sr is null ) continue;
			var c = sr.Color;
			c.a += (target - c.a) * step;
			sr.Color = c;
		}
	}

	/// <summary>Spawn one gooey drip. Off the trailing edge while moving (so the block leaves it behind in
	/// open space), else off a random edge. A little outward nudge, then it falls (DRIP_GRAVITY) and
	/// shrinks away.</summary>
	void EmitDrip()
	{
		bool moving = !IsStopped && _moveSpeed > DRIP_MOVING_SPEED;
		Direction edge = moving
			? Globals.GetOppositeDirection( MoveDirection )   // trailing edge -> always left visible behind us
			: Globals.GetAllDirections()[Rng.CosmeticInt( 0, 4 )];

		Vector2 outward = Globals.GetVectorForDirection( edge );
		Vector2 along = new Vector2( -outward.y, outward.x ); // across the face
		Vector2 pos = Position + outward * (Width / 2f) + along * Rng.CosmeticFloat( -Width / 2f, Width / 2f );

		// A small outward spray; when moving the block itself supplies the separation, so keep it gentle.
		Vector2 vel = outward * Rng.CosmeticFloat( 6f, 20f ) + along * Rng.CosmeticFloat( -8f, 8f );

		ParticleKind kind = Rng.CosmeticInt( 0, 3 ) == 0 ? ParticleKind.StickyGoo1 : ParticleKind.StickyGoo0;
		Stage.AddWindParticle( pos, vel, decel: 0.94f, gravity: DRIP_GRAVITY, kind, Rng.CosmeticFloat( 0.35f, 0.7f ), Rng.CosmeticInt( 2, 5 ) );
	}

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