Conveyance/PeachCannon.cs
using Sandbox;

/// <summary>
/// Environment Peach Cannon — lobs GIANT Moonlight Peaches in a ballistic arc.
/// Fires GiantPeachPrefab (PeachProjectile with IsGiant=true).
/// Supports Autonomous tracking or PlayerTriggered via TriggerFire().
/// </summary>
[Title( "Peach Cannon" )]
[Category( "Game / Peach" )]
public sealed class PeachCannon : Component
{
	public enum CannonMode { Autonomous, PlayerTriggered }

	// ── Wiring ────────────────────────────────────────────────────────────────
	[Property] public GameObject GiantPeachPrefab { get; set; }
	[Property] public GameObject Muzzle           { get; set; }
	[Property] public GameObject BarrelPivot      { get; set; }

	// ── Targeting ─────────────────────────────────────────────────────────────
	[Property] public string TargetTag                           { get; set; } = "player";
	[Property, Range( 200f, 8000f )] public float DetectionRange { get; set; } = 3000f;
	[Property, Range( 10f,  360f  )] public float TurnRate       { get; set; } = 35f;

	// ── Firing ────────────────────────────────────────────────────────────────
	[Property, Range( 2f,  30f   )] public float FireInterval { get; set; } = 7f;
	[Property, Range( 200f, 2000f)] public float PeachSpeed   { get; set; } = 650f;
	[Property, Range( 50f, 2000f )] public float PeachMass    { get; set; } = 500f;
	[Property, Range( 0f,  5f    )] public float PeachHealth  { get; set; } = 0f;

	// ── Ballistics ────────────────────────────────────────────────────────────
	[Property] public bool UseBallisticAim                    { get; set; } = true;
	[Property, Range( 1, 5 )] public int BallisticIterations { get; set; } = 3;

	// ── Mode ──────────────────────────────────────────────────────────────────
	[Property] public CannonMode OperationMode { get; set; } = CannonMode.Autonomous;

	// ── Runtime ───────────────────────────────────────────────────────────────
	private TimeSince  _timeSinceFired;
	private GameObject _currentTarget;
	private bool       _pendingTriggeredFire = false;

	/// <summary>Call from a pressure plate / lever to fire one shot (PlayerTriggered mode).</summary>
	public void TriggerFire() => _pendingTriggeredFire = true;

	protected override void OnUpdate()
	{
		if ( IsProxy ) return;

		_currentTarget = FindNearestTarget();

		if ( _currentTarget.IsValid() && BarrelPivot.IsValid() )
			TrackTarget( _currentTarget );

		bool shouldFire = OperationMode switch
		{
			CannonMode.Autonomous      => _timeSinceFired >= FireInterval && _currentTarget.IsValid() && IsAimed(),
			CannonMode.PlayerTriggered => _pendingTriggeredFire && _currentTarget.IsValid(),
			_                          => false
		};

		if ( shouldFire )
		{
			FireGiantPeach();
			_timeSinceFired       = 0;
			_pendingTriggeredFire = false;
		}
	}

	// ── Targeting ─────────────────────────────────────────────────────────────

	private GameObject FindNearestTarget()
	{
		GameObject best  = null;
		float bestDistSq = DetectionRange * DetectionRange;

		foreach ( var obj in Scene.GetAllObjects( true ) )
		{
			if ( !obj.Tags.Has( TargetTag ) ) continue;
			float d = (obj.WorldPosition - WorldPosition).LengthSquared;
			if ( d < bestDistSq ) { bestDistSq = d; best = obj; }
		}

		return best;
	}

	private Vector3 GetAimPoint( GameObject target )
	{
		var r = target.Components.Get<ModelRenderer>( FindMode.EnabledInSelfAndDescendants );
		return r.IsValid() ? r.Bounds.Center : target.WorldPosition;
	}

	private Vector3 ComputeBallisticAimDirection( Vector3 targetPos )
	{
		var muzzlePos = Muzzle.IsValid() ? Muzzle.WorldPosition : BarrelPivot.WorldPosition;
		float gravity = -Scene.PhysicsWorld.Gravity.z;
		if ( gravity <= 0f ) return (targetPos - muzzlePos).Normal;

		var aimPoint = targetPos;
		for ( int i = 0; i < BallisticIterations; i++ )
		{
			float dist = (aimPoint - muzzlePos).Length;
			float tof  = dist / PeachSpeed;
			float drop = 0.5f * gravity * tof * tof;
			aimPoint   = targetPos + Vector3.Up * drop;
		}

		return (aimPoint - muzzlePos).Normal;
	}

	private void TrackTarget( GameObject target )
	{
		var aimDir = UseBallisticAim && Muzzle.IsValid()
			? ComputeBallisticAimDirection( GetAimPoint( target ) )
			: (GetAimPoint( target ) - BarrelPivot.WorldPosition).Normal;

		var desired = Rotation.LookAt( aimDir );
		BarrelPivot.WorldRotation = BarrelPivot.WorldRotation.Clamp( desired, TurnRate * Time.Delta );
	}

	private bool IsAimed()
	{
		if ( !BarrelPivot.IsValid() || !_currentTarget.IsValid() ) return false;
		var aimDir = UseBallisticAim && Muzzle.IsValid()
			? ComputeBallisticAimDirection( GetAimPoint( _currentTarget ) )
			: (GetAimPoint( _currentTarget ) - BarrelPivot.WorldPosition).Normal;
		return Vector3.Dot( BarrelPivot.WorldRotation.Forward, aimDir ) > 0.97f;
	}

	// ── Fire ──────────────────────────────────────────────────────────────────

	private void FireGiantPeach()
	{
		if ( !GiantPeachPrefab.IsValid() || !Muzzle.IsValid() ) return;

		var peach = GiantPeachPrefab.Clone( Muzzle.WorldPosition, Muzzle.WorldRotation );
		peach.NetworkSpawn();

		var rb = peach.Components.Get<Rigidbody>( FindMode.EnabledInSelfAndDescendants );
		if ( rb.IsValid() )
		{
			rb.MassOverride = PeachMass;
			rb.Velocity     = Muzzle.WorldRotation.Forward * PeachSpeed;
		}

		var proj = peach.Components.Get<PeachProjectile>( FindMode.EnabledInSelfAndDescendants );
		if ( proj.IsValid() )
		{
			proj.IsGiant   = true;
			proj.MaxHealth = PeachHealth;
		}

		Sound.Play( "sounds/explosion_near", WorldPosition );
	}
}