Weapons/PeachLauncher/PeachLauncherWeapon.cs
using Sandbox.Rendering;

/// <summary>
/// Peach Launcher — fires Moonlight Peach projectiles as a proper BaseWeapon
/// inventory item inside PlayerInventory's slot system.
///
/// ARCHITECTURE
/// ─────────────
/// Bone parenting, animgraph HoldType, and proxy visibility are all handled
/// by BaseCarryable.CreateWorldModel() — it clones WorldModelPrefab locally
/// on every client marked NotNetworked, so there is no transform sync conflict.
///
/// Projectile spawning is routed through [Rpc.Host] so the host always
/// performs the NetworkSpawn — see RpgWeapon for the same pattern.
///
/// PREFAB SETUP (peach_launcher.prefab)
/// ─────────────────────────────────────
///   Root GO components:
///     PeachLauncherWeapon   ← this script
///     Rigidbody             ← disabled while held via SetDropped()
///     ModelCollider         ← same
///     DroppedWeapon         ← press-to-pickup tooltip
///
///   Inspector — BaseCarryable section:
///     WorldModelPrefab  → weapons/peach_launcher/peach_launcher_worldmodel.prefab
///     HoldType          → Pistol
///     ParentBone        → hold_r
///
///   Inspector — BaseWeapon / Ammo section:
///     UsesAmmo          → true
///     UsesClips         → false
///     MaxReserveAmmo    → 5
///     StartingAmmo      → 5
///
///   Inspector — this script:
///     PeachProjectilePrefab → your moonlightpeachprojectile.prefab
/// </summary>
[Title( "Peach Launcher Weapon" )]
[Category( "Game / Peach" )]
public class PeachLauncherWeapon : BaseWeapon
{
	// ── Wiring ────────────────────────────────────────────────────────────────
	[Property] public GameObject PeachProjectilePrefab { get; set; }
	[Property] public GameObject GiantPeachProjectilePrefab { get; set; }

	// ── Tuning ────────────────────────────────────────────────────────────────
	[Property, Range( 100f, 5000f )] public float PeachSpeed      { get; set; } = 1400f;
	[Property, Range( 10f,  500f  )] public float PeachMass       { get; set; } = 60f;
	[Property, Range( 0f,   10f   )] public float PeachHealth     { get; set; } = 3f;
	[Property, Range( 0f,   20f   )] public float Spread          { get; set; } = 1.5f;
	[Property, Range( 10f,  200f  )] public float MuzzleOffset    { get; set; } = 60f;
	[Property, Range( 0.2f, 5f    )] public float FireRate        { get; set; } = 0.6f;

	// Giant peach lob tuning
	[Property, Range( 100f, 2000f )] public float GiantPeachSpeed { get; set; } = 600f;
	[Property, Range( 10f,  500f  )] public float GiantPeachMass  { get; set; } = 200f;
	[Property, Range( 0f,   10f   )] public float GiantPeachHealth { get; set; } = 0f; // splats on first impact
	[Property, Range( 0.5f, 5f    )] public float GiantFireRate   { get; set; } = 1.5f;

	// ── BaseWeapon overrides ───────────────────────────────────────────────────

	protected override float GetPrimaryFireRate() => FireRate;

	// Single shot per click — not hold-to-spam
	protected override bool WantsPrimaryAttack() => Input.Pressed( "attack1" );

	// No iron sights / secondary mode
	public override bool CanSecondaryAttack() => HasAmmo() && !IsReloading() && TimeUntilNextShotAllowed <= 0;
	protected override bool WantsSecondaryAttack() => Input.Pressed( "attack2" );

	public override void SecondaryAttack()
	{
		if ( !TakeAmmo( 1 ) ) return;

		AddShootDelay( GiantFireRate );
		WeaponModel?.OnAttack();

		// Compute aim values locally on the firing client (so spread/origin use
		// this player's camera) then RPC the host to actually spawn the peach.
		var (spawnPos, forward) = GetProjectileOrigin();
		var lobbed = (forward + Vector3.Up * 0.4f).Normal;

		SpawnGiantPeachOnHost( spawnPos, lobbed );
	}

	public override void PrimaryAttack()
	{
		if ( !TakeAmmo( 1 ) ) return;

		AddShootDelay( FireRate );
		WeaponModel?.OnAttack();

		// Compute spawn position and direction locally on the firing client so
		// spread, muzzle position and camera direction reflect THIS player's
		// view. Then RPC the host to perform the authoritative NetworkSpawn.
		var (spawnPos, forward) = GetProjectileOrigin();
		forward = (forward + Vector3.Random * (Spread * MathF.PI / 180f)).Normal;

		SpawnPeachOnHost( spawnPos, forward );
	}

	// ── Host-authoritative spawning ──────────────────────────────────────────
	//
	// These follow the same pattern as RpgWeapon.CreateProjectile — the host
	// performs the NetworkSpawn so the projectile exists on every client.

	[Rpc.Host]
	private void SpawnPeachOnHost( Vector3 spawnPos, Vector3 forward )
	{
		if ( !PeachProjectilePrefab.IsValid() )
		{
			Log.Warning( "PeachLauncherWeapon: PeachProjectilePrefab is not assigned!" );
			return;
		}

		var peach = PeachProjectilePrefab.Clone( new Transform( spawnPos, Rotation.LookAt( forward ) ) );
		peach.NetworkSpawn();

		if ( peach.Components.Get<Rigidbody>( FindMode.EnabledInSelfAndDescendants ) is { } rb )
		{
			rb.MassOverride = PeachMass;
			rb.Velocity     = forward * PeachSpeed;
		}

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

	[Rpc.Host]
	private void SpawnGiantPeachOnHost( Vector3 spawnPos, Vector3 lobbed )
	{
		var prefab = GiantPeachProjectilePrefab.IsValid() ? GiantPeachProjectilePrefab : PeachProjectilePrefab;
		if ( !prefab.IsValid() )
		{
			Log.Warning( "PeachLauncherWeapon: No giant peach prefab assigned!" );
			return;
		}

		var peach = prefab.Clone( new Transform( spawnPos, Rotation.LookAt( lobbed ) ) );
		peach.NetworkSpawn();

		if ( peach.Components.Get<Rigidbody>( FindMode.EnabledInSelfAndDescendants ) is { } rb )
		{
			rb.MassOverride = GiantPeachMass;
			rb.Velocity     = lobbed * GiantPeachSpeed;
		}

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

	/// <summary>
	/// Returns the correct projectile spawn position and aim direction for both
	/// first and third person.
	///
	/// The problem: in third person AimRay.Position is the CAMERA position
	/// (behind the player's head), not the muzzle. Spawning a Rigidbody there
	/// puts it inside the player's own collider.
	///
	/// The solution: always spawn from the player's eye (or muzzle if available),
	/// but aim in the direction of the camera ray so the crosshair is accurate.
	/// Then nudge the spawn point forward enough to clear the player capsule.
	/// </summary>
	protected (Vector3 position, Vector3 forward) GetProjectileOrigin()
	{
		var forward = AimRay.Forward; // always correct direction

		// Use the muzzle transform if available, otherwise the player's eye.
		// Both are on the player, not the camera — safe for Rigidbody spawning.
		Vector3 spawnPos;
		if ( WeaponModel?.MuzzleTransform.IsValid() ?? false )
		{
			spawnPos = WeaponModel.MuzzleTransform.WorldPosition;
		}
		else if ( HasOwner )
		{
			spawnPos = Owner.EyeTransform.Position + forward * MuzzleOffset;
		}
		else
		{
			spawnPos = AimRay.Position + forward * MuzzleOffset;
		}

		return (spawnPos, forward);
	}

	// ── Crosshair ─────────────────────────────────────────────────────────────

	public override void DrawCrosshair( HudPainter hud, Vector2 center )
	{
		var canShoot = HasAmmo() && !IsReloading() && TimeUntilNextShotAllowed <= 0;
		var color    = canShoot ? CrosshairCanShoot : CrosshairNoShoot;

		hud.SetBlendMode( BlendMode.Normal );
		hud.DrawCircle( center, 6, Color.Black );
		hud.DrawCircle( center, 4, color );
	}

	// ── Dry fire ──────────────────────────────────────────────────────────────

	public override void OnControl( Player player )
	{
		base.OnControl( player );

		if ( Input.Pressed( "attack1" ) && !HasAmmo() )
			DryFire();
	}
}