things/HealingZone.cs

A HealingZone Thing component that spawns particle effects and periodically heals Players and Enemies inside its collider. It tracks lifetime, stops emitter near end, spawns visual rings on heal ticks, and plays a heal sound when someone is healed.

NetworkingFile Access
using System;
using Sandbox;

public class HealingZone : Thing
{
	[Property] public ParticleEmitter Emitter { get; set; }

	public float Damage { get; set; }
	[Sync] public float Lifetime { get; set; }

	private Dictionary<Thing, float> _healTimes;
	private const float HEAL_INTERVAL = 0.75f;

	private bool _hasStoppedSpawning;

	private TimeSince _timeSinceHeal;

	public float HealAmount { get; set; }

	protected override void OnStart()
	{
		base.OnStart();

		_timeSinceHeal = 0f;

		Lifetime = 60f; // todo: should be able to overlap with other events?

		if ( IsProxy )
			return;

		CollideWithTags.Add( "player" );
		CollideWithTags.Add( "enemy" );

		_healTimes = new();

		HealAmount = 5f;
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		bool shouldHeal = false;

		if( _timeSinceHeal > HEAL_INTERVAL && TimeSinceSpawn > 1f && TimeSinceSpawn < Lifetime - 0.5f )
		{
			Manager.Instance.SpawnRing( Position2D, Radius, new Color( 0.1f, 1f, 0.1f, 0.25f ), lifetime: HEAL_INTERVAL * 1.25f, path: "ring2" );

			shouldHeal = true;

			_timeSinceHeal = 0f;
		}

		if ( !_hasStoppedSpawning && TimeSinceSpawn > Lifetime - 5f )
		{
			Emitter.Loop = false;
			_hasStoppedSpawning = true;
		}

		if ( Manager.Instance.IsGameOver )
			return;

		if ( IsProxy )
			return;

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"{_damageTimes.Count}", new global::Transform( WorldPosition ) );

		for(int i = _healTimes.Count - 1; i >= 0; i-- )
		{
			var pair = _healTimes.ElementAt(i);
			if ( Time.Now > pair.Value + HEAL_INTERVAL )
				_healTimes.Remove( pair.Key );
		}

		if ( TimeSinceSpawn > Lifetime )
			GameObject.Destroy();

		var playSfx = false;

		if( shouldHeal )
		{
			foreach( var other in Touching )
			{
				if ( other is Enemy enemy && !enemy.IsSpawning && !enemy.IsDying && !enemy.IsInTheAir )
				{
					if( enemy.Health < enemy.MaxHealth )
						playSfx = true;

					enemy.Heal( HealAmount );
					
				}
				else if ( other is Player player && !player.IsDead && !player.IsInTheAir )
				{
					if ( player.Health < player.GetSyncStat( PlayerStat.MaxHp ) )
						playSfx = true;

					player.HealRpc( HealAmount );
				}
			}
		}

		if ( playSfx )
		{
			Manager.Instance.PlaySfxNearbyRpc( "heal", Position2D, pitch: Game.Random.Float(1.4f, 1.5f), volume: 0.4f, maxDist: 250f );
		}
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		//if ( TimeSinceSpawn < 0.35f || TimeSinceSpawn > Lifetime - 0.33f || _healTimes.ContainsKey(other) )
		//	return;

		//if ( other is Unit unit && !unit.IsDying && !unit.IsInTheAir )
		//{
		//	if ( other is Enemy e && e.IsSpawning )
		//		return;

		//	//if ( unit is Enemy enemy )
		//	//{
		//	//	enemy.DamageRpc( Damage, PlayerSource, DamageType.Fire, new Vector3( hitPos.x, hitPos.y, 30f ), Vector2.Zero, isCrit: false, shouldFlinch: false );
		//	//}
		//	//else if ( unit is Player player )
		//	//{
		//	//	player.DamageRpc( Damage, damageType: DamageType.Fire, hitPos, dir, 0f, EnemySource );
		//	//}

		//	_healTimes.Add( other, Time.Now );
		//}
	}

	public void SetScale( float scale )
	{
		var sphereEmitter = GetComponent<ParticleSphereEmitter>();
		sphereEmitter.Radius *= scale;
		sphereEmitter.Rate = new ParticleFloat { Type = ParticleFloat.ValueType.Constant, ConstantValue = sphereEmitter.Rate.ConstantValue * scale };

		var sphereCollider = Collider as SphereCollider;
		sphereCollider.Radius *= scale;

		//var particleEffect = GetComponent<ParticleEffect>();
		//var range = new ParticleFloat { Type = ParticleFloat.ValueType.Range, ConstantA = 50f * scale, ConstantB = 70f * scale };
		//particleEffect.Scale = range;

		Radius = sphereCollider.Radius;
	}
}