Weapons/PeachLauncher/PeachSplat.cs
/// <summary>
/// The residue left when a Moonlight Peach hits a surface.
/// Lives on the splat prefab alongside FadeAndDestroy (which handles its visual death).
///
/// Contains a trigger collider that detects players walking through the splat zone.
/// While inside, the local player receives:
///   - A movement slow (horizontal velocity damped each FixedUpdate)
///   - A screen overlay via PeachOverlayBridge → PeachSplatOverlay.razor
///
/// The effect is LOCAL — evaluated per-client, so many simultaneous splats across
/// the map don't fight each other.
/// </summary>
[Title( "Peach Splat Zone" )]
[Category( "Game / Peach" )]
public sealed class PeachSplat : Component, Component.ITriggerListener
{
	// ── Identity ───────────────────────────────────────────────────────────────
	[Property] public bool IsGiant { get; set; } = false;

	// ── Slow ───────────────────────────────────────────────────────────────────
	[Property, Range( 0.05f, 0.99f )] public float VelocityRetention { get; set; } = 0.55f;
	[Property, Range( 0f,    5f    )] public float LingerTime         { get; set; } = 1.2f;

	// ── Overlay ────────────────────────────────────────────────────────────────
	[Property, Range( 0f,    1f    )] public float OverlayIntensity { get; set; } = 0.55f;
	[Property, Range( 0.05f, 3f    )] public float OverlayFadeSpeed { get; set; } = 0.6f;

	// ── Runtime ────────────────────────────────────────────────────────────────
	private readonly HashSet<GameObject> _playersInside = new();

	// ── ITriggerListener ───────────────────────────────────────────────────────

	void Component.ITriggerListener.OnTriggerEnter( Collider self, GameObject other )
	{
		if ( !IsLocalPlayer( other ) ) return;

		_playersInside.Add( other );
		PeachOverlayBridge.EnterZone( IsGiant, OverlayIntensity, OverlayFadeSpeed );
	}

	void Component.ITriggerListener.OnTriggerExit( Collider self, GameObject other )
	{
		if ( !_playersInside.Remove( other ) ) return;

		PeachOverlayBridge.ExitZone( LingerTime, OverlayFadeSpeed );
	}

	// ── Per-frame slow ─────────────────────────────────────────────────────────

	protected override void OnFixedUpdate()
	{
		foreach ( var player in _playersInside )
		{
			if ( !player.IsValid() ) continue;

			var cc = player.Components.Get<CharacterController>( FindMode.EnabledInSelfAndDescendants );
			if ( cc is null ) continue;

			var vel  = cc.Velocity;
			float damp = 1f - (1f - VelocityRetention.Clamp( 0f, 1f )) * Time.Delta * 3f;
			cc.Velocity = new Vector3( vel.x * damp, vel.y * damp, vel.z );
		}
	}

	// ── Helpers ────────────────────────────────────────────────────────────────

	private static bool IsLocalPlayer( GameObject go )
	{
		if ( !go.Tags.Has( "player" ) ) return false;
		return go.Components
			.GetAll<Component>( FindMode.EverythingInSelfAndDescendants )
			.Any( c => !c.IsProxy );
	}
}