Conveyance/TriggerAreaEffect.cs
using Sandbox;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Drop on any trigger volume (Collider with IsTrigger = true) to apply a
/// continuous or instant effect to entities that enter it.
///
/// EFFECT TYPES
///   InstantDeath  — kills the player immediately on entry (sharks, lava)
///   DamagePerSec  — deals damage every second while inside (fire, acid)
///   SlowZone      — slows Players, PatrolMovers, and SimpleAnimalControllers
///   PushZone      — applies a constant directional force (wind, updraft)
///
/// NETWORKING — execution machine per effect type
///   InstantDeath, DamagePerSec → Host-only.
///       Player.OnDamage is authoritative on the host; calling it from a
///       remote client has no effect, which is why the older IsProxy guard
///       silently dropped damage for non-host players.
///   SlowZone → Every machine.
///       PlayerController.WalkSpeed is read locally on each client, so each
///       machine needs to apply the slow to its own simulation. Tickets are
///       tracked per-target so overlaps compose cleanly on each side.
///   PushZone → Host-only.
///       Rigidbody.Velocity changes should originate on the host so the
///       network-replicated physics state stays authoritative.
///
/// SLOW ZONE — overlap handling
///   Overlapping slow zones do NOT stack multiplicatively. If two zones with
///   multipliers 0.5 and 0.3 both contain you, your effective speed is 0.3×
///   (the minimum), not 0.15×.
///
/// SLOW ZONE — supported targets
///   • Players                  — slows Walk/Run/Ducked speeds on PlayerController
///   • PatrolMovers             — slows the Speed field
///   • SimpleAnimalControllers  — slows SpeedWalk/SpeedFollow + NavMeshAgent.MaxSpeed
/// </summary>
[Category( "Conveyance" ), Icon( "dangerous" )]
public sealed class TriggerAreaEffect : Component, Component.ITriggerListener
{
	// -------------------------------------------------------------------------
	// Configuration
	// -------------------------------------------------------------------------

	public enum AreaEffectType { InstantDeath, DamagePerSec, SlowZone, PushZone }

	[Property] public AreaEffectType EffectType { get; set; } = AreaEffectType.InstantDeath;

	[Property] public TagSet AffectTags { get; set; } = new TagSet( new[] { "player" } );

	[Property, Group( "Damage" )] public string DamageTag       { get; set; } = "trigger";
	[Property, Group( "Damage" )] public float  DamagePerSecond { get; set; } = 25f;

	[Property, Group( "Slow Zone" ), Range( 0.05f, 1f )]
	public float SpeedMultiplier { get; set; } = 0.35f;

	[Property, Group( "Push Zone" )] public Vector3 PushDirection { get; set; } = Vector3.Up;
	[Property, Group( "Push Zone" )] public float   PushPower     { get; set; } = 20f;

	// -------------------------------------------------------------------------
	// Slow tracking — global, shared across every TriggerAreaEffect instance
	// -------------------------------------------------------------------------

	private sealed class SlowState
	{
		public readonly Dictionary<GameObject, float> Tickets = new();

		public Player Player;
		public PlayerController Controller;
		public PatrolMover Patrol;
		public SimpleAnimalController Animal;

		public float BaseWalkSpeed;
		public float BaseRunSpeed;
		public float BaseDuckedSpeed;
		public float BasePatrolSpeed;
		public float BaseAnimalWalk;
		public float BaseAnimalFollow;
	}

	private static readonly Dictionary<GameObject, SlowState> SlowStates = new();

	// -------------------------------------------------------------------------
	// State for *this* instance (non-slow effects)
	// -------------------------------------------------------------------------

	private readonly HashSet<Player> _inside = new();

	// -------------------------------------------------------------------------
	// Trigger callbacks
	// -------------------------------------------------------------------------

	void ITriggerListener.OnTriggerEnter( Collider other )
	{
		var go = other.GameObject.Root;
		if ( !go.Tags.HasAny( AffectTags ) ) return;

		// Damage and push are host-authoritative — only run there.
		// Slow zone runs on every machine so each client's local PlayerController
		// sees the speed change.
		bool needsHost = EffectType is AreaEffectType.InstantDeath
		              or AreaEffectType.DamagePerSec
		              or AreaEffectType.PushZone;
		if ( needsHost && !Networking.IsHost ) return;

		switch ( EffectType )
		{
			case AreaEffectType.InstantDeath:
			{
				var player = go.Components.Get<Player>();
				if ( player.IsValid() ) ApplyInstantDeath( player );
				break;
			}

			case AreaEffectType.SlowZone:
				AddSlowTicket( go );
				break;

			case AreaEffectType.DamagePerSec:
			case AreaEffectType.PushZone:
			{
				var player = go.Components.Get<Player>();
				if ( player.IsValid() ) _inside.Add( player );
				break;
			}
		}
	}

	void ITriggerListener.OnTriggerExit( Collider other )
	{
		var go = other.GameObject.Root;

		// Match the enter-side authority rules so we don't leave dangling state.
		bool needsHost = EffectType is AreaEffectType.InstantDeath
		              or AreaEffectType.DamagePerSec
		              or AreaEffectType.PushZone;
		if ( needsHost && !Networking.IsHost ) return;

		if ( EffectType == AreaEffectType.SlowZone )
		{
			RemoveSlowTicket( go );
			return;
		}

		var player = go.Components.Get<Player>();
		if ( player.IsValid() ) _inside.Remove( player );
	}

	// -------------------------------------------------------------------------
	// Continuous effects (damage / push) — only ever tick on the host
	// -------------------------------------------------------------------------

	protected override void OnFixedUpdate()
	{
		if ( _inside.Count == 0 ) return;
		if ( !Networking.IsHost ) return;

		_inside.RemoveWhere( p => !p.IsValid() );

		foreach ( var player in _inside )
		{
			switch ( EffectType )
			{
				case AreaEffectType.DamagePerSec:  ApplyDamagePerSec( player ); break;
				case AreaEffectType.PushZone:      ApplyPush( player );         break;
			}
		}
	}

	// -------------------------------------------------------------------------
	// Damage / push implementations
	// -------------------------------------------------------------------------

	private void ApplyInstantDeath( Player player )
	{
		var dmg = new DamageInfo( 9999f, GameObject, GameObject );
		dmg.Tags.Add( DamageTag );
		player.OnDamage( in dmg );
	}

	private void ApplyDamagePerSec( Player player )
	{
		var dmg = new DamageInfo( DamagePerSecond * Time.Delta, GameObject, GameObject );
		dmg.Tags.Add( DamageTag );
		player.OnDamage( in dmg );
	}

	private void ApplyPush( Player player )
	{
		var pc = player.Controller;
		if ( pc.IsValid() ) pc.PreventGrounding( 0.1f );

		var rb = player.Components.Get<Rigidbody>( FindMode.EverythingInSelfAndParent );
		if ( rb.IsValid() ) rb.Velocity += PushDirection * PushPower;
	}

	// -------------------------------------------------------------------------
	// Slow ticket — add / remove / recompose
	// -------------------------------------------------------------------------

	private void AddSlowTicket( GameObject target )
	{
		if ( !SlowStates.TryGetValue( target, out var state ) )
		{
			state = CreateSlowState( target );
			if ( state == null ) return;
			SlowStates[target] = state;
		}

		state.Tickets[GameObject] = SpeedMultiplier;
		Recompose( target, state );
	}

	private void RemoveSlowTicket( GameObject target )
	{
		if ( !SlowStates.TryGetValue( target, out var state ) ) return;

		state.Tickets.Remove( GameObject );

		if ( state.Tickets.Count == 0 )
		{
			RestoreBaselines( state );
			SlowStates.Remove( target );
		}
		else
		{
			Recompose( target, state );
		}
	}

	private static SlowState CreateSlowState( GameObject target )
	{
		var state = new SlowState
		{
			Player  = target.Components.Get<Player>(),
			Patrol  = target.Components.Get<PatrolMover>(),
			Animal  = target.Components.Get<SimpleAnimalController>(),
		};

		bool any = false;

		if ( state.Player.IsValid() && state.Player.Controller.IsValid() )
		{
			state.Controller      = state.Player.Controller;
			state.BaseWalkSpeed   = state.Controller.WalkSpeed;
			state.BaseRunSpeed    = state.Controller.RunSpeed;
			state.BaseDuckedSpeed = state.Controller.DuckedSpeed;
			any = true;
		}

		if ( state.Patrol.IsValid() )
		{
			state.BasePatrolSpeed = state.Patrol.Speed;
			any = true;
		}

		if ( state.Animal.IsValid() )
		{
			state.BaseAnimalWalk   = state.Animal.SpeedWalk;
			state.BaseAnimalFollow = state.Animal.SpeedFollow;
			any = true;
		}

		return any ? state : null;
	}

	private static void Recompose( GameObject target, SlowState state )
	{
		float min = state.Tickets.Values.Min();

		if ( state.Controller.IsValid() )
		{
			state.Controller.WalkSpeed   = state.BaseWalkSpeed   * min;
			state.Controller.RunSpeed    = state.BaseRunSpeed    * min;
			state.Controller.DuckedSpeed = state.BaseDuckedSpeed * min;
		}

		if ( state.Patrol.IsValid() )
			state.Patrol.Speed = state.BasePatrolSpeed * min;

		if ( state.Animal.IsValid() )
		{
			state.Animal.SpeedWalk   = state.BaseAnimalWalk   * min;
			state.Animal.SpeedFollow = state.BaseAnimalFollow * min;
		}
	}

	private static void RestoreBaselines( SlowState state )
	{
		if ( state.Controller.IsValid() )
		{
			state.Controller.WalkSpeed   = state.BaseWalkSpeed;
			state.Controller.RunSpeed    = state.BaseRunSpeed;
			state.Controller.DuckedSpeed = state.BaseDuckedSpeed;
		}

		if ( state.Patrol.IsValid() )
			state.Patrol.Speed = state.BasePatrolSpeed;

		if ( state.Animal.IsValid() )
		{
			state.Animal.SpeedWalk   = state.BaseAnimalWalk;
			state.Animal.SpeedFollow = state.BaseAnimalFollow;
		}
	}

	// -------------------------------------------------------------------------
	// Cleanup — when a slow zone is disabled / destroyed, drop its tickets
	// -------------------------------------------------------------------------

	protected override void OnDisabled()
	{
		foreach ( var kv in SlowStates.ToList() )
		{
			if ( kv.Value.Tickets.Remove( GameObject ) )
			{
				if ( kv.Value.Tickets.Count == 0 )
				{
					RestoreBaselines( kv.Value );
					SlowStates.Remove( kv.Key );
				}
				else
				{
					Recompose( kv.Key, kv.Value );
				}
			}
		}

		_inside.Clear();
	}

	// -------------------------------------------------------------------------
	// Editor gizmo
	// -------------------------------------------------------------------------

	protected override void DrawGizmos()
	{
		Gizmo.Draw.Color = EffectType switch
		{
			AreaEffectType.InstantDeath => Color.Red.WithAlpha( 0.25f ),
			AreaEffectType.DamagePerSec => Color.Orange.WithAlpha( 0.25f ),
			AreaEffectType.SlowZone     => Color.Cyan.WithAlpha( 0.25f ),
			AreaEffectType.PushZone     => Color.Green.WithAlpha( 0.25f ),
			_                           => Color.White.WithAlpha( 0.1f ),
		};

		if ( EffectType == AreaEffectType.PushZone )
			Gizmo.Draw.Arrow( Vector3.Zero, PushDirection * 60f );
	}
}