Entities/Blocks/BlockSquid.cs
namespace BlockParty;

/// <summary>
/// Squid block — on a hard impact (speed > 50) at phase 1+ it fires a laser: a harmless
/// aiming "tracer" beam for 2s, then a deadly beam (5s at phase 1, 10s at phase 2; the
/// phase-2 beam sweeps/rotates). The beam stops at the first block or arena wall, and kills
/// the player if it crosses them during the deadly phase. Port of the original BlockSquid.
///
/// Rendering: the original drew animated variable-width line segments + endpoint sparks. We
/// reproduce it with pixel sprites rolled in the view plane (SpriteLayer.FlatRotation) so each
/// is a rotated rectangle laid end-to-end along the beam — a solid line, not a chain of squares.
/// Each segment carries the original's travelling width-pulse. The deadly beam adds a wide
/// flickering yellow glow halo behind the red core plus radial spark bursts at both endpoints;
/// the tracer is a thin grey line.
/// </summary>
public class BlockSquid : Block
{
	enum LaserState { Idle, Tracer, Laser }
	LaserState _state = LaserState.Idle;
	protected override bool CanActivateTimedAbility => _state == LaserState.Idle;
	float _timer;
	float _laserDuration;
	Vector2 _laserDir;
	float _laserRotSpeed, _targetRotSpeed;
	Line _currentLine;
	float _sfxTimer;

	const float TRACER_TIME = 2.0f;
	const float LASER_LENGTH = 384.0f;
	const float ROT_SPEED = 25.0f;
	const float SFX_TRACER_TIME = 1.25f;
	const float SFX_LASER_TIME = 1.0f;

	// Beam rendering. The original drew the beam as a run of short variable-width line
	// segments whose widths cycle through a pattern; the pattern index shifts each frame so a
	// "pulse" ripples outward along the beam. We reproduce that with rotated rectangles placed
	// at fixed increments (see PlaceSeg), plus — for the deadly beam — a wide yellow glow halo
	// behind the red core and spark bursts flickering at both endpoints.
	const float BEAM_INCREMENT = 4.0f;   // spacing between red-core samples
	const int NUM_CORE = 96;             // covers the full cast, including a piercing variant's wall overshoot
	const float GLOW_INCREMENT = 8.0f;   // glow is wide, so it can sample more coarsely
	const int NUM_GLOW = 48;
	const int NUM_SPARK = 24;            // 12 sparks per endpoint (6 yellow + 6 red)

	// Beam local-Z stack (childOrder * SpriteLayer.LAYER_Z_STEP=0.1, on top of DEPTH_BLOCK=2):
	// glow behind core behind sparks, and the whole beam just above the block's face layers.
	const int GLOW_CHILD_ORDER = 6;
	const int CORE_CHILD_ORDER = 7;
	const int SPARK_CHILD_ORDER = 8;
	// The last few samples can sit below DEPTH_BLOCK so a far impact is visually masked by blocks.
	// Keep this gated by source distance so close impacts never slip under the squid that fired them.
	const float UNDER_BLOCK_TAIL_LENGTH = 24f;
	const float UNDER_BLOCK_MIN_SOURCE_DISTANCE = 32f;
	const int GLOW_UNDER_BLOCK_CHILD_ORDER = -4;
	const int CORE_UNDER_BLOCK_CHILD_ORDER = -3;
	const int SPARK_UNDER_BLOCK_CHILD_ORDER = -2;

	readonly List<SpriteRenderer> _coreSegs = new();
	readonly List<SpriteRenderer> _glowSegs = new();
	readonly List<SpriteRenderer> _sparkSegs = new();

	// Travelling width pulse. Per segment i the width is widths[(counter + i) % widths.Length];
	// the counter decrements every LASER_WIDTH_TIME so the pulse flows outward from the eye.
	static readonly int[] LASER_WIDTHS = { 2, 4, 6, 8, 6, 4 };
	static readonly int[] TRACER_WIDTHS = { 1, 1, 2, 3, 2, 1 };
	static readonly float[] TRACER_PULSE_AMOUNTS = { 0f, 0.04f, 0.12f, 0.22f, 0.12f, 0.04f };
	const float LASER_WIDTH_TIME = 0.033f;
	int _widthCounter;
	float _widthTimer;

	// Cosmetic-only randomness (glow flicker + spark scatter). Kept separate from the sim
	// Rng so frame-rate-dependent render jitter can never desync the simulation.
	readonly Random _renderRng = new();

	static readonly Color LASER_TRACER_COLOR = new Color( 35 / 255f, 45 / 255f, 55 / 255f ); // GREY
	// Pure saturated red/yellow rather than the original palette's RED(200,90,85)/YELLOW(221,221,153):
	// s&box's rendering reads those palette swatches as washed-out, so the beam looked desaturated
	// next to the old version. Fully-saturated primaries restore the hot laser look.
	static readonly Color LASER_RED_COLOR = new Color( 1f, 0f, 0f );      // pure red
	static readonly Color LASER_YELLOW_COLOR = new Color( 1f, 1f, 0f );   // pure yellow
	protected virtual Color BeamTracerColor => LASER_TRACER_COLOR;
	protected virtual Color BeamCoreColor => LASER_RED_COLOR;
	protected virtual Color BeamGlowColor => LASER_YELLOW_COLOR;
	protected virtual float BeamArenaOvershoot => 0f;
	protected virtual bool RendersTailUnderBlocks => true;
	protected virtual float BeamGlowLocalZ( bool underBlocks )
		=> ChildOrderToLocalZ( underBlocks ? GLOW_UNDER_BLOCK_CHILD_ORDER : GLOW_CHILD_ORDER );
	protected virtual float BeamCoreLocalZ( bool underBlocks )
		=> ChildOrderToLocalZ( underBlocks ? CORE_UNDER_BLOCK_CHILD_ORDER : CORE_CHILD_ORDER );
	protected virtual float BeamSparkLocalZ( bool underBlocks )
		=> ChildOrderToLocalZ( underBlocks ? SPARK_UNDER_BLOCK_CHILD_ORDER : SPARK_CHILD_ORDER );
	protected virtual bool CanBeamKill( Player player ) => true;

	/// <summary>Surfaces a fresh aim won't fire into (its beam would die on the spot, or leave the arena
	/// immediately). Blocks aren't considered — they move — only the static obstacles and arena walls;
	/// the piercing variant pierces obstacles, so it only avoids walls.</summary>
	protected virtual AimAvoid FreshAimAvoids => AimAvoid.WallsAndObstacles;

	Vector2 GetEyePosition() => Position + new Vector2( 0, 6f );

	public override void CreateVisuals()
	{
		base.CreateVisuals();
		BuildPool( _glowSegs, NUM_GLOW, GLOW_CHILD_ORDER );
		BuildPool( _coreSegs, NUM_CORE, CORE_CHILD_ORDER );
		BuildPool( _sparkSegs, NUM_SPARK, SPARK_CHILD_ORDER );
	}

	void BuildPool( List<SpriteRenderer> pool, int count, int childOrder )
	{
		for ( int i = 0; i < count; i++ )
		{
			var sr = SpriteLayer.Add( GameObject, "sprites/pixel.sprite", new Vector2( 1, 1 ), "idle", childOrder );
			// Each segment is rolled along the beam (see PlaceSeg): a solid rotated line rather than a
			// chain of axis-aligned squares. (pixel.sprite is a 1x1 texture, so Size maps directly to
			// the quad's width/height with no aspect correction.)
			sr.Enabled = false;
			pool.Add( sr );
		}
	}

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

		if ( _moveSpeed > 50.0f && (Phase == 1 || Phase == 2) )
			StartLaser( Phase == 1 ? 5.0f : 10.0f );
		else
			CloseEyesAndPickNewDirection();
	}

	void StartLaser( float duration )
	{
		// Aim toward arena center with a random ±90° spread. If the eye sits exactly on the center,
		// toCenter is zero — rotating and normalizing both preserve zero, which would leave a
		// zero-length beam for the whole sequence — so fall back to a valid direction (matching
		// BlockLaser.AimDirectionForPhase) before applying the spread.
		Vector2 toCenter = Arena.Center - GetEyePosition();
		Vector2 baseDir = toCenter.LengthSquared > 0.0001f ? toCenter : new Vector2( 0f, -1f );
		_laserDir = Utils.Normalized( Utils.RotateVector( baseDir, Rng.Float( -90f, 90f ) ) );
		// Don't fire into a wall/obstacle we're flush against — the beam would die at birth (a squid
		// wedged in a corner shooting the corner). Mirrors the aim off that face instead.
		_laserDir = DeflectAimOffSurfaces( GetEyePosition(), _laserDir, FreshAimAvoids );
		_targetRotSpeed = ROT_SPEED * Utils.RandomSign();
		// NOT a bug — the ternary is not inverted. Tick gates rotation on the live Phase, which can
		// flip 1->2 mid-beam. Phase 2 seeds 0f because Tick's Tracer branch ramps _laserRotSpeed up
		// from zero. Phase 1 never rotates (Tick skips it), so this value is unused *unless* the block
		// phases up to 2. If that happens during the deadly Laser state — where nothing recomputes
		// _laserRotSpeed — it uses this seed as-is, so it must be _targetRotSpeed (full speed, matching
		// a native phase-2 deadly beam). Seeding 0f here would leave a promoted beam frozen.
		_laserRotSpeed = Phase == 2 ? 0f : _targetRotSpeed;
		_laserDuration = duration;
		_state = LaserState.Tracer;
		_timer = TRACER_TIME;
		_sfxTimer = 0f;
		SuppressEyeBlink = true;
		_eyes.PlayAnimation( $"eyes_shooting_{Phase}" );
	}

	public override void Tick( float dt )
	{
		base.Tick( dt );
		if ( IsDead || _state == LaserState.Idle ) return;

		// Rotate the beam (phase 2). Ramp up rotation during the tracer.
		if ( Phase == 2 )
		{
			if ( _state == LaserState.Tracer )
			{
				float elapsed = TRACER_TIME - _timer;
				_laserRotSpeed = Utils.Map( elapsed, 0f, TRACER_TIME, 0f, _targetRotSpeed, true, EasingType.SineEaseOut );
			}
			_laserDir = Utils.Normalized( Utils.RotateVector( _laserDir, _laserRotSpeed * dt ) );
		}

		ComputeBeam();

		if ( _state == LaserState.Laser )
		{
			AddShake( Utils.RandomVector() );

			// The beam kills any player body it touches — the real player AND any Summoner impostors.
			foreach ( var player in Stage.LivingPlayers ) HitBody( player );
			foreach ( var imp in Stage.Impostors ) HitBody( imp );

			void HitBody( Player body )
			{
				if ( body is not null && !body.IsDead && CanBeamKill( body ) && _currentLine.Intersects( body.GetRect() ) )
					body.KilledByLaser( Utils.Normalized( body.Position - _currentLine.A ) );
			}
		}

		// Beam sfx.
		_sfxTimer -= dt;
		if ( _sfxTimer <= 0f )
		{
			if ( _state == LaserState.Tracer ) { Audio.PlaySfx( SfxType.LaserTracer, Position, 0.25f ); _sfxTimer = SFX_TRACER_TIME; }
			else { Audio.PlaySfx( SfxType.Laser, Position, 0.33f ); _sfxTimer = SFX_LASER_TIME; }
		}

		_timer -= dt;
		if ( _timer <= 0f )
		{
			if ( _state == LaserState.Tracer )
			{
				_state = LaserState.Laser;
				_timer = _laserDuration;
				_sfxTimer = 0f;
			}
			else
			{
				_state = LaserState.Idle;
				SuppressEyeBlink = false;
				HideBeam();
				CloseEyesAndPickNewDirection();
				return;
			}
		}

		RenderBeam( dt );
	}

	protected virtual IEnumerable<Entity2D> BeamBlockers()
	{
		foreach ( Block block in Stage.GetBlocks() )
		{
			if ( block == this || block.PhasingIn ) continue;
			yield return block;
		}

		foreach ( Entity2D obstacle in Stage.GetSolidObstacles() )
			yield return obstacle;
	}

	void ComputeBeam()
	{
		Vector2 start = GetEyePosition();
		var ray = new Line( start, start + _laserDir * LASER_LENGTH );

		float minDistSqr = float.MaxValue;
		Vector2 end = Vector2.Zero;
		bool hit = false;

		foreach ( Entity2D blocker in BeamBlockers() )
		{
			if ( ray.Intersects( blocker.GetRect(), out Vector2 p ) )
			{
				float d = (p - start).LengthSquared;
				if ( d < minDistSqr ) { minDistSqr = d; end = p; hit = true; }
			}
		}

		if ( !hit )
		{
			// Cast to the outer arena edge (x=0/WIDTH, y=0/HEIGHT). The beam draws full-length
			// chunks past this point; the pillarbox edge-blockers (StageBase) mask the overshoot so
			// the in-bounds beam stays full-length without visibly poking outside the arena.
			float w = Arena.WIDTH, h = Arena.HEIGHT;
			Vector2 p;
			if ( ray.Intersects( new Line( new Vector2( 0, 0 ), new Vector2( w, 0 ) ), out p ) ) end = p;
			else if ( ray.Intersects( new Line( new Vector2( 0, 0 ), new Vector2( 0, h ) ), out p ) ) end = p;
			else if ( ray.Intersects( new Line( new Vector2( w, 0 ), new Vector2( w, h ) ), out p ) ) end = p;
			else if ( ray.Intersects( new Line( new Vector2( 0, h ), new Vector2( w, h ) ), out p ) ) end = p;
			else end = ray.B;

			end += _laserDir * BeamArenaOvershoot;
		}

		_currentLine = new Line( start, end );
	}

	void RenderBeam( float dt )
	{
		// Advance the travelling width pulse (decrement so it flows outward from the eye).
		_widthTimer -= dt;
		if ( _widthTimer < 0f )
		{
			_widthCounter = (_widthCounter - 1 + LASER_WIDTHS.Length) % LASER_WIDTHS.Length;
			_widthTimer = LASER_WIDTH_TIME;
		}

		bool deadly = _state == LaserState.Laser;
		float len = _currentLine.Length;
		Vector2 dir = len > 0.001f ? (_currentLine.B - _currentLine.A) / len : Vector2.Zero;

		// Glow halo + endpoint sparks are part of the deadly beam only; the tracer is a thin grey
		// line with no glow or sparks (matching the original DrawLaser(tracer:true) path).
		if ( deadly )
		{
			// Yellow halo behind the beam, flickering its thickness each frame for a hot,
			// unstable look. Floor it above the core's peak width (8) so the yellow always frames
			// the red core — otherwise the core out-widths the halo at pulse peaks and those spots
			// read as detached orange chunks rather than one continuous yellow beam.
			float glowWidth = RenderFloat( 9f, 13f );
			PlaceLine( _glowSegs, dir, len, GLOW_INCREMENT, glowWidth, BeamGlowColor );
			DrawSparks( _currentLine.A, 0, false );
			DrawSparks( _currentLine.B, NUM_SPARK / 2, ShouldRenderEndpointUnderBlocks( len ) );
		}
		else
		{
			HidePool( _glowSegs );
			HidePool( _sparkSegs );
		}

		// Red core (deadly) / grey tracer with the pulsing width pattern on top of the glow.
		int[] widths = deadly ? LASER_WIDTHS : TRACER_WIDTHS;
		for ( int i = 0; i < _coreSegs.Count; i++ )
		{
			var sr = _coreSegs[i];
			float dist = (i + 0.5f) * BEAM_INCREMENT;
			if ( dist - BEAM_INCREMENT * 0.5f > len )
			{
				sr.Enabled = false;
				continue;
			}
			int pulseIndex = (_widthCounter + i) % widths.Length;
			int w = widths[pulseIndex];
			Color coreColor = deadly
				? BeamCoreColor
				: Color.Lerp( BeamTracerColor, Color.White, TRACER_PULSE_AMOUNTS[pulseIndex] );
			// +1 length overlap seals the seams between consecutive segments.
			PlaceSeg( sr, _currentLine.A + dir * dist, dir, BEAM_INCREMENT + 1f, w, coreColor,
				BeamCoreLocalZ( ShouldRenderTailUnderBlocks( GlowCellDist( dist ), len ) ) );
		}
	}

	// Core samples every 4 units, glow every 8: decide the under-block handoff per GLOW cell so a
	// glow segment straddling the tail boundary never stays above core samples that already
	// dropped under it (that read as a yellow band cutting the red core near the impact).
	static float GlowCellDist( float dist ) => (MathF.Floor( dist / GLOW_INCREMENT ) + 0.5f) * GLOW_INCREMENT;

	// Lay a solid run of rotated rectangles along the beam, all the same width/colour.
	void PlaceLine( List<SpriteRenderer> pool, Vector2 dir, float len, float increment, float width, Color color )
	{
		for ( int i = 0; i < pool.Count; i++ )
		{
			float dist = (i + 0.5f) * increment;
			if ( dist - increment * 0.5f > len ) { pool[i].Enabled = false; continue; }
			PlaceSeg( pool[i], _currentLine.A + dir * dist, dir, increment + 1f, width, color,
				BeamGlowLocalZ( ShouldRenderTailUnderBlocks( dist, len ) ) );
		}
	}

	// A flickering burst of sparks radiating from an endpoint: 6 large yellow + 6 small red,
	// each a short rotated line pointing outward, repositioned every frame (mirrors the
	// original DrawLaserSparks, which drew short radial lines each render).
	void DrawSparks( Vector2 origin, int offset, bool underBlocks )
	{
		float localZ = BeamSparkLocalZ( underBlocks );
		for ( int i = 0; i < 6; i++ )
		{
			Vector2 d = RenderUnitVector();
			float length = RenderFloat( 4f, 12f );
			PlaceSeg( _sparkSegs[offset + i], origin + d * (length * 0.5f), d, length, RenderFloat( 2f, 3.5f ), BeamGlowColor, localZ );
		}
		for ( int i = 0; i < 6; i++ )
		{
			Vector2 d = RenderUnitVector();
			float length = RenderFloat( 2f, 8f );
			PlaceSeg( _sparkSegs[offset + 6 + i], origin + d * (length * 0.5f), d, length, RenderFloat( 1f, 2f ), BeamCoreColor, localZ );
		}
	}

	protected virtual bool ShouldRenderTailUnderBlocks( float distFromSource, float beamLength )
	{
		return RendersTailUnderBlocks
			&& distFromSource >= UNDER_BLOCK_MIN_SOURCE_DISTANCE
			&& distFromSource >= beamLength - UNDER_BLOCK_TAIL_LENGTH;
	}

	protected virtual bool ShouldRenderEndpointUnderBlocks( float beamLength )
	{
		return RendersTailUnderBlocks && beamLength >= UNDER_BLOCK_MIN_SOURCE_DISTANCE;
	}

	static float ChildOrderToLocalZ( int childOrder ) => childOrder * SpriteLayer.LAYER_Z_STEP;

	// Position/size/orient one segment: a (length x width) rectangle centred at worldPos with its
	// long (Size.x) axis pointing along dir (see SpriteLayer.FlatRotation). Sprites live in WORLD
	// space but the block root is also world-positioned, so convert to local position.
	void PlaceSeg( SpriteRenderer sr, Vector2 worldPos, Vector2 dir, float length, float width, Color color, float localZ )
	{
		sr.Enabled = true;
		sr.Color = color;
		sr.Size = new Vector2( length, width );
		sr.GameObject.LocalRotation = SpriteLayer.FlatRotation( new Vector2( -dir.y, dir.x ) );
		Vector2 local = worldPos - Pos;
		sr.GameObject.LocalPosition = new Vector3( local.x, local.y, localZ );
	}

	float RenderFloat( float min, float max ) => min + (float)_renderRng.NextDouble() * (max - min);

	Vector2 RenderUnitVector()
	{
		float a = RenderFloat( 0f, MathF.PI * 2f );
		return new Vector2( MathF.Cos( a ), MathF.Sin( a ) );
	}

	static void HidePool( List<SpriteRenderer> pool )
	{
		foreach ( var sr in pool ) sr.Enabled = false;
	}

	void HideBeam()
	{
		HidePool( _coreSegs );
		HidePool( _glowSegs );
		HidePool( _sparkSegs );
	}

	public override void Die()
	{
		base.Die();
		_state = LaserState.Idle;
		SuppressEyeBlink = false;
		HideBeam();
	}
}