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