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; }
}