Conveyance/DragonTurret.cs
using Sandbox;

/// <summary>
/// A dumb but lovable autonomous turret. Scans for tagged targets within range,
/// smoothly tracks the nearest one with its Gun child, and fires Bullet prefabs
/// from its Muzzle on a fixed cadence. No damage — just physics knockback.
///
/// Now with **gunnery sergeant ballistics**: when UseBallisticAim is on (the default),
/// the turret accounts for projectile drop under gravity and aims its barrel *upward*
/// from the target by the amount the bullet will fall during its flight. The result is
/// a parabolic shot that actually hits a stationary target instead of plonking into the
/// dirt halfway there.
///
/// How the ballistic solver works:
///   1. Estimate time-of-flight T from launcher to target (rough horizontal distance / speed).
///   2. Compute how far gravity will pull the bullet down during T: drop = 0.5 * g * T^2.
///   3. Aim the barrel at (target_position + Up * drop) — shoot HIGHER than the target.
///   4. Recompute T using the new aim distance, refine drop, repeat 2-3 iterations.
///   The fixed-point iteration converges fast because the elevation angle changes only
///   slightly each pass once we're close to the right answer.
///
/// This iteration-based approach has two big wins over the closed-form quadratic solution:
///   - No square root call (s&box sandboxes most of System.*, so float.Sqrt /
///     MathF.Sqrt may or may not be available depending on environment). We compute
///     everything from Vector3.Length and basic arithmetic, both of which are bulletproof.
///   - Trivial to extend to moving targets later: just predict where the target will
///     be at time T using their velocity, instead of using their static position.
/// </summary>
public sealed class DragonTurret : Component
{
	// === Wiring ===

	/// <summary>
	/// The child GameObject that visually rotates to aim. Usually called "Gun".
	/// </summary>
	[Property] public GameObject Gun { get; set; }

	/// <summary>
	/// The child GameObject (usually under Gun) where projectiles spawn from.
	/// </summary>
	[Property] public GameObject Muzzle { get; set; }

	/// <summary>
	/// Prefab to spawn as a projectile. Should have a Rigidbody.
	/// </summary>
	[Property] public GameObject BulletPrefab { get; set; }

	/// <summary>
	/// Optional gun model whose tint flashes on each shot. Pure juice.
	/// Set both this AND GunColorGradient for the flash to fire.
	/// </summary>
	[Property] public ModelRenderer GunModel { get; set; }

	/// <summary>
	/// Colour gradient sampled by the gun flash. X-axis is seconds-since-fired * 2.
	/// Ignored unless GunModel is also set.
	/// </summary>
	[Property] public Gradient GunColorGradient { get; set; }

	// === Targeting ===

	/// <summary>
	/// Tag a GameObject must carry to be considered a target. Defaults to "player".
	/// </summary>
	[Property] public string TargetTag { get; set; } = "player";

	/// <summary>
	/// How far away (in units) the turret can see targets.
	/// </summary>
	[Property, Range( 100, 5000 )] public float DetectionRange { get; set; } = 1500f;

	/// <summary>
	/// Degrees per second the gun can rotate while tracking. Lower = more dodgeable.
	/// </summary>
	[Property, Range( 30, 720 )] public float TurnRateDegPerSec { get; set; } = 120f;

	// === Firing ===

	/// <summary>
	/// Seconds between shots.
	/// </summary>
	[Property, Range( 0.1f, 5f )] public float FireInterval { get; set; } = 1.5f;

	/// <summary>
	/// Muzzle velocity of the bullet, in units/sec.
	/// </summary>
	[Property, Range( 200, 5000 )] public float BulletSpeed { get; set; } = 1500f;

	/// <summary>
	/// Mass assigned to the bullet's Rigidbody at spawn time, overriding whatever's in the prefab.
	/// Heavier bullets = more knockback. Player Rigidbody is mass 500 by default,
	/// so 50–200 feels meaningful without being silly.
	/// </summary>
	[Property, Range( 1, 500 )] public float BulletMass { get; set; } = 100f;

	/// <summary>
	/// Optional sound to play on fire.
	/// </summary>
	[Property] public SoundEvent FireSound { get; set; }

	// === Ballistics ===

	/// <summary>
	/// If true, the turret accounts for gravity drop and lobs its shots in a parabolic
	/// arc to actually hit the target. If false, falls back to the old "aim straight at
	/// the target" behaviour, which is funny but rarely lands a hit past close range.
	/// </summary>
	[Property] public bool UseBallisticAim { get; set; } = true;

	/// <summary>
	/// How many refinement passes the ballistic solver does before committing to an aim.
	/// 2-3 is plenty for typical engagement ranges. More iterations don't hurt but stop
	/// improving the result. Lower numbers (1) leave the aim slightly short for long shots.
	/// </summary>
	[Property, Range( 1, 6 )] public int BallisticIterations { get; set; } = 3;

	// === Runtime ===

	private TimeSince timeSinceFired = 0;
	private GameObject currentTarget;

	protected override void OnUpdate()
	{
		FlashGunModel();

		currentTarget = FindNearestTarget();

		if ( currentTarget.IsValid() )
		{
			TrackTarget( currentTarget );

			if ( timeSinceFired >= FireInterval && IsAimedAtTarget( currentTarget ) )
			{
				Fire();
				timeSinceFired = 0;
			}
		}
	}

	/// <summary>
	/// Scan all GameObjects tagged with TargetTag and return the closest one in range.
	/// </summary>
	private GameObject FindNearestTarget()
	{
		GameObject best = null;
		float bestDistSq = DetectionRange * DetectionRange;

		foreach ( var candidate in Scene.GetAllObjects( true ) )
		{
			if ( !candidate.Tags.Has( TargetTag ) )
				continue;

			float distSq = ( candidate.WorldPosition - WorldPosition ).LengthSquared;
			if ( distSq < bestDistSq )
			{
				bestDistSq = distSq;
				best = candidate;
			}
		}

		return best;
	}

	/// <summary>
	/// Return the world-space point we should be aiming at on the target.
	/// Uses the renderer bounds centre (chest-ish for a citizen, middle for any model)
	/// rather than the GameObject origin (which is usually at ground level).
	/// </summary>
	private Vector3 GetAimPoint( GameObject target )
	{
		// ModelRenderer (and its subclass SkinnedModelRenderer) both expose Bounds.
		var renderer = target.Components.Get<ModelRenderer>( FindMode.EnabledInSelfAndDescendants );
		if ( renderer.IsValid() )
		{
			return renderer.Bounds.Center;
		}
		// Fallback: object origin.
		return target.WorldPosition;
	}

	/// <summary>
	/// Compute the direction the gun should point to hit `targetPos`, accounting for
	/// gravity drop along the way. Returns a normalized direction vector.
	///
	/// This is the heart of the ballistic upgrade. We iterate:
	///   - Estimate time-of-flight to where we're currently aiming
	///   - Predict how much the bullet will drop during that time
	///   - Aim higher by that much
	///   - Repeat
	/// Each pass refines both the predicted time and the predicted drop together,
	/// converging on the actual ballistic solution.
	///
	/// If the target is "unreachable" (too far for our bullet speed to compensate for
	/// gravity), the solver will produce an aim direction that approaches vertical —
	/// effectively giving up and shooting straight up. We could detect this and fall
	/// back to direct aim, but in practice "aim higher and hope" is the right behaviour
	/// for a dumb-but-lovable turret.
	/// </summary>
	private Vector3 ComputeBallisticAimDirection( Vector3 targetPos )
	{
		// Gravity is a Vector3 on the physics world; we only care about its magnitude
		// since the maths assumes a single down-axis. In standard s&box it's around 800
		// units/s² downward. Reading it from the world keeps us robust to scene-level
		// gravity overrides.
		var gravityVec = Scene.PhysicsWorld.Gravity;
		float gravity = -gravityVec.z; // gravityVec.z is negative for "down"; we want positive magnitude.
		if ( gravity <= 0f )
		{
			// Zero or upward gravity? Just aim straight. No drop to compensate for.
			return (targetPos - Muzzle.WorldPosition).Normal;
		}

		// Start with a direct-aim guess. Each iteration will refine it upward by the
		// predicted drop at the current estimated flight time.
		var aimPoint = targetPos;

		for ( int i = 0; i < BallisticIterations; i++ )
		{
			// Distance from muzzle to the current aim point. This is what the bullet
			// actually has to travel along its launch direction, so it's the correct
			// number to divide by speed for time-of-flight.
			float distance = (aimPoint - Muzzle.WorldPosition).Length;
			float timeOfFlight = distance / BulletSpeed;

			// Standard projectile drop: y = 0.5 * g * t^2 below the launch line.
			// We compensate by aiming HIGHER than the actual target by this much.
			float drop = 0.5f * gravity * timeOfFlight * timeOfFlight;

			aimPoint = targetPos + Vector3.Up * drop;
		}

		return (aimPoint - Muzzle.WorldPosition).Normal;
	}

	/// <summary>
	/// Decide where the gun should be pointing to hit this target. Either ballistic
	/// (lobs to compensate for gravity) or direct (straight line), depending on toggle.
	/// </summary>
	private Vector3 GetDesiredAimDirection( GameObject target )
	{
		var targetPos = GetAimPoint( target );

		if ( UseBallisticAim && Muzzle.IsValid() )
			return ComputeBallisticAimDirection( targetPos );

		// Direct aim — original behaviour.
		var sourcePos = Muzzle.IsValid() ? Muzzle.WorldPosition : Gun.WorldPosition;
		return (targetPos - sourcePos).Normal;
	}

	/// <summary>
	/// Rotate the Gun toward the target at a capped angular speed.
	/// Uses Rotation.Clamp which gives us "current rotation, but no more than N degrees from target".
	/// </summary>
	private void TrackTarget( GameObject target )
	{
		if ( !Gun.IsValid() ) return;

		var aimDir = GetDesiredAimDirection( target );
		var desiredRotation = Rotation.LookAt( aimDir );

		var maxStepDeg = TurnRateDegPerSec * Time.Delta;
		Gun.WorldRotation = Gun.WorldRotation.Clamp( desiredRotation, maxStepDeg );
	}

	/// <summary>
	/// True if the gun is pointing close enough to the target's BALLISTIC aim direction
	/// that firing makes sense. Prevents the turret from spraying wildly while still slewing,
	/// AND from firing the moment it lines up with the target's center while its actual
	/// trajectory is still way off (since the ballistic aim point is above the target).
	/// </summary>
	private bool IsAimedAtTarget( GameObject target )
	{
		if ( !Gun.IsValid() ) return false;

		var aimDir = GetDesiredAimDirection( target );
		var gunDir = Gun.WorldRotation.Forward;
		// Dot > 0.95 ≈ within ~18 degrees of aim.
		return Vector3.Dot( gunDir, aimDir ) > 0.95f;
	}

	/// <summary>
	/// Spawn a bullet at the muzzle, propel it forward, play sound, mark fire time.
	///
	/// Note: this method doesn't need to know anything about ballistics. The muzzle's
	/// forward direction is whatever the gun is pointing at right now, which is already
	/// the elevated ballistic aim if UseBallisticAim is on. Bullet inherits that and gravity
	/// does the rest — a beautiful side effect of the gun-rotation system being purely
	/// transform-based.
	/// </summary>
	private void Fire()
	{
		if ( !BulletPrefab.IsValid() || !Muzzle.IsValid() ) return;

		var bullet = BulletPrefab.Clone( Muzzle.WorldPosition, Muzzle.WorldRotation );

		var rb = bullet.Components.Get<Rigidbody>( FindMode.EnabledInSelfAndDescendants );
		if ( rb.IsValid() )
		{
			// MassOverride > 0 tells the engine "use this mass instead of computing from colliders".
			// Setting at spawn time guarantees the value is applied, regardless of prefab state.
			rb.MassOverride = BulletMass;
			rb.Velocity = Muzzle.WorldRotation.Forward * BulletSpeed;
		}

		if ( FireSound is not null )
		{
			Sound.Play( FireSound, Muzzle.WorldPosition );
		}
	}

	/// <summary>
	/// Tint the gun model briefly after a shot, sampling the gradient.
	/// Only runs if both GunModel and GunColorGradient are wired up.
	/// </summary>
	private void FlashGunModel()
	{
		if ( !GunModel.IsValid() ) return;

		// timeSinceFired in [0, 0.5] maps to gradient X in [0, 1]
		GunModel.Tint = GunColorGradient.Evaluate( timeSinceFired * 2.0f );
	}
}