Weapons/PeachLauncher/PeachAmmoSource.cs
/// <summary>
/// Peach Tree (or any ammo source) — players entering the trigger zone
/// have their PeachLauncherWeapon reserve topped up via AddReserveAmmo().
///
/// Two modes:
///   Passive  — tops up every TickInterval seconds while inside
///   OneShot  — gives ammo once on entry, recharges after TickInterval
///
/// Wire up:
///   Add a SphereCollider or BoxCollider (IsTrigger = true) on this GO or a child.
///   Place PeachAmmoSource on the same GO as the collider's parent.
/// </summary>
[Title( "Peach Ammo Source" )]
[Category( "Game / Peach" )]
public sealed class PeachAmmoSource : Component, Component.ITriggerListener
{
	public enum FillMode { Passive, OneShot }

	// ── Settings ───────────────────────────────────────────────────────────────
	[Property, Range( 1, 20   )] public int   MaxAmmo      { get; set; } = 5;
	[Property, Range( 1, 5    )] public int   AmmoPerTick  { get; set; } = 1;
	[Property, Range( 0.1f, 10f )] public float TickInterval { get; set; } = 1.0f;
	[Property] public FillMode Mode         { get; set; } = FillMode.Passive;
	[Property] public GameObject PickupEffect { get; set; }

	// ── Runtime ────────────────────────────────────────────────────────────────
	private readonly HashSet<GameObject> _playersInside = new();
	private TimeSince _timeSinceTick;
	private bool _oneShotReady = true;

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

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

		if ( Mode == FillMode.OneShot && _oneShotReady )
		{
			GiveAmmo( other );
			_oneShotReady = false;
		}
	}

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

	// ── Tick ──────────────────────────────────────────────────────────────────

	protected override void OnUpdate()
	{
		if ( _timeSinceTick < TickInterval ) return;
		_timeSinceTick = 0;

		if ( Mode == FillMode.OneShot )
		{
			if ( !_oneShotReady ) _oneShotReady = true;
			return;
		}

		foreach ( var player in _playersInside )
		{
			if ( player.IsValid() )
				GiveAmmo( player );
		}
	}

	// ── Ammo logic ─────────────────────────────────────────────────────────────

	private void GiveAmmo( GameObject player )
	{
		// Weapon lives as a child of the player GO inside PlayerInventory —
		// EverythingInSelfAndDescendants is required to find it.
		var launcher = player.Components.Get<PeachLauncherWeapon>( FindMode.EverythingInSelfAndDescendants );
		if ( !launcher.IsValid() ) return;

		var added = launcher.AddReserveAmmo( AmmoPerTick );
		if ( added > 0 && PickupEffect.IsValid() )
			PickupEffect.Clone( WorldPosition );
	}

	// ── Helper ─────────────────────────────────────────────────────────────────

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