Weapons/PeachLauncher/SplatReceiver.cs
/// <summary>
/// SplatReceiver — attach to any GameObject that should react when hit by a peach.
///
/// USAGE
/// ──────
/// Drop this component on any GO (turret, crate, peach tree, player, wall tile).
/// Wire the OnSplatted action graph slot in the inspector to define what happens:
///   - Play a particle effect
///   - Change a material colour
///   - Disable/enable another component
///   - Trigger an animation state
///   - Call a method on a sibling component
///   - Anything the action graph can express
///
/// For code-side reactions (e.g. turret stun), implement ISplatReactor on any
/// component on this GameObject — SplatReceiver finds and calls them all
/// automatically without needing a reference.
///
/// PeachProjectile calls ReceiveSplat() on the surface it hits.
/// PeachSplatZone calls ReceiveSplat() on objects that enter its trigger.
/// </summary>
[Title( "Splat Receiver" )]
[Category( "Game / Peach" )]
public sealed class SplatReceiver : Component
{
	// ── Inspector ─────────────────────────────────────────────────────────────

	/// <summary>
	/// Action graph slot — wire this in the inspector to define custom splat
	/// reactions per-prefab without writing any code.
	/// Receives the SplatInfo as a graph variable named "Splat".
	/// </summary>
	[Property] public Action<SplatInfo> OnSplatted { get; set; }

	/// <summary>
	/// If true, only the most recent splat counts — a second hit while already
	/// splatted resets the timer rather than stacking effects.
	/// </summary>
	[Property] public bool ResetOnResplat { get; set; } = true;

	/// <summary>
	/// How long the splat state lasts before clearing. 0 = permanent until
	/// manually cleared via ClearSplat().
	/// </summary>
	[Property, Range( 0f, 30f )] public float SplatDuration { get; set; } = 10f;

	// ── State ─────────────────────────────────────────────────────────────────

	public bool IsSplatted { get; private set; } = false;
	public SplatInfo LastSplat { get; private set; }

	private TimeSince _timeSinceSplat;

	// ── API ───────────────────────────────────────────────────────────────────

	/// <summary>
	/// Called by PeachProjectile on direct impact, or PeachSplatZone on overlap.
	/// Fires the action graph and notifies all ISplatReactor components.
	/// </summary>
	public void ReceiveSplat( SplatInfo info )
	{
		if ( IsSplatted && !ResetOnResplat ) return;

		IsSplatted     = true;
		LastSplat      = info;
		_timeSinceSplat = 0;

		// Fire the action graph wired in the inspector
		OnSplatted?.Invoke( info );

		// Notify all code-side reactors on this GameObject and its children
		foreach ( var reactor in Components.GetAll<ISplatReactor>( FindMode.EverythingInSelfAndDescendants ) )
			reactor.OnSplatReceived( info );
	}

	/// <summary>
	/// Manually clear the splat state — useful for turret cooldown logic.
	/// </summary>
	public void ClearSplat()
	{
		if ( !IsSplatted ) return;
		IsSplatted = false;

		foreach ( var reactor in Components.GetAll<ISplatReactor>( FindMode.EverythingInSelfAndDescendants ) )
			reactor.OnSplatCleared();
	}

	// ── Auto-clear ────────────────────────────────────────────────────────────

	protected override void OnUpdate()
	{
		if ( !IsSplatted ) return;
		if ( SplatDuration <= 0f ) return;

		if ( _timeSinceSplat >= SplatDuration )
			ClearSplat();
	}
}

/// <summary>
/// Implement this interface on any component to receive splat events from
/// a SplatReceiver on the same GameObject, without needing an inspector reference.
///
/// Example: TurretComponent implements ISplatReactor to stun itself on splat.
/// </summary>
public interface ISplatReactor
{
	void OnSplatReceived( SplatInfo info );
	void OnSplatCleared();
}

/// <summary>
/// Data passed with every splat event — what kind of peach, where it hit,
/// who fired it, and whether it was a direct hit or zone overlap.
/// </summary>
public struct SplatInfo
{
	/// <summary>World position of the splat impact.</summary>
	public Vector3 Position { get; set; }

	/// <summary>Surface normal at the impact point.</summary>
	public Vector3 Normal { get; set; }

	/// <summary>The player who fired the peach, if any.</summary>
	public Player Attacker { get; set; }

	/// <summary>True if this was a direct projectile hit; false if from zone overlap.</summary>
	public bool IsDirectHit { get; set; }

	/// <summary>True if the peach was a giant peach.</summary>
	public bool IsGiant { get; set; }
}