Conveyance/GoalZone.cs
using Sandbox;

/// <summary>
/// A delivery target. When a GameObject tagged "prop" enters the trigger volume,
/// the goal pad changes colour, plays a sound, and the prop is disabled.
///
/// NEW: When DeliveryCount reaches DeliveryThreshold, fires the same targets
/// as InteractButton — Bridge.Toggle(), CableCar.ToggleRunning(), or
/// ToggleTarget.Enabled = true. This means a bascule bridge wired to RewardBridge
/// will lower itself as a door the moment enough props are delivered, no extra code.
///
/// The reward fires ONCE when the threshold is first crossed. Set RewardRepeats = true
/// to fire again every time another full threshold worth of props arrives.
///
/// SETUP:
///   1. BoxCollider (IsTrigger = true) + ModelRenderer on this GO as before.
///   2. Set DeliveryThreshold to however many props unlock the reward (default 1).
///   3. Drag the same targets you'd use on an InteractButton into the Reward group.
/// </summary>
public sealed class GoalZone : Component, Component.ITriggerListener
{
	// -------------------------------------------------------------------------
	// Existing properties (unchanged)
	// -------------------------------------------------------------------------

	[Property] public Gradient SuccessColor  { get; set; }
	[Property] public SoundEvent DeliverySound { get; set; }
	[Property] public string AcceptedTag { get; set; } = "prop";

	[Property] public int DeliveryCount { get; private set; }

	// -------------------------------------------------------------------------
	// Reward threshold
	// -------------------------------------------------------------------------

	/// <summary>How many props must be delivered before the reward fires.</summary>
	[Property, Group( "Reward" )]
	public int DeliveryThreshold { get; set; } = 1;

	/// <summary>
	/// If true the reward fires again every time another ThresholdCount props arrive
	/// (e.g. every 3 props opens the bridge again after it has been closed).
	/// If false (default) the reward fires only the first time the threshold is reached.
	/// </summary>
	[Property, Group( "Reward" )]
	public bool RewardRepeats { get; set; } = false;

	// -------------------------------------------------------------------------
	// Reward targets — same pattern as InteractButton
	// -------------------------------------------------------------------------

	/// <summary>Calls Toggle() on a BasculeBridge — works as a door opening.</summary>
	[Property, Group( "Reward" )] public BasculeBridge RewardBridge { get; set; }

	/// <summary>Calls ToggleRunning() on a JointAnchorMover (cable car / platform).</summary>
	[Property, Group( "Reward" )] public JointAnchorMover RewardCableCar { get; set; }

	/// <summary>Sets Enabled = true on anything — turret, windmill, light, particle system.</summary>
	[Property, Group( "Reward" )] public Component RewardToggleTarget { get; set; }

	/// <summary>Optional sound played when the reward fires (separate from per-delivery sound).</summary>
	[Property, Group( "Reward" )] public SoundEvent RewardSound { get; set; }

	// -------------------------------------------------------------------------
	// Private state
	// -------------------------------------------------------------------------

	private bool _rewardFired;

	// -------------------------------------------------------------------------
	// Trigger
	// -------------------------------------------------------------------------

	void Component.ITriggerListener.OnTriggerEnter( Collider other )
	{
		if ( !other.GameObject.Tags.Has( AcceptedTag ) ) return;

		DeliveryCount++;

		// Per-delivery visual + sound (unchanged behaviour)
		foreach ( var r in Components.GetAll<ModelRenderer>( FindMode.EnabledInSelfAndDescendants ) )
			r.Tint = SuccessColor.Evaluate( Game.Random.Float() );

		if ( DeliverySound is not null )
			Sound.Play( DeliverySound, WorldPosition );

		other.GameObject.Enabled = false;

		Log.Info( $"[GoalZone] Delivery #{DeliveryCount}: {other.GameObject.Name}" );

		// Check whether we've hit the threshold
		bool hitThreshold = RewardRepeats
			? DeliveryCount % DeliveryThreshold == 0
			: DeliveryCount == DeliveryThreshold;

		if ( hitThreshold && (!_rewardFired || RewardRepeats) )
		{
			FireReward();
			_rewardFired = true;
		}
	}

	// -------------------------------------------------------------------------
	// Reward
	// -------------------------------------------------------------------------

	private void FireReward()
	{
		Log.Info( $"[GoalZone] Threshold {DeliveryThreshold} reached — firing reward" );

		RewardBridge?.Toggle();
		RewardCableCar?.ToggleRunning();

		if ( RewardToggleTarget.IsValid() )
			RewardToggleTarget.Enabled = !RewardToggleTarget.Enabled;

		if ( RewardSound is not null )
			Sound.Play( RewardSound, WorldPosition );
	}
}