unitStatus/UnitStatusPoison.cs

UnitStatusPoison is a UnitStatus-derived component that applies periodic poison damage to a Unit (player or enemy), tracks accumulated damage, handles interactions with fire, spreading on death, and finishing damage when the status is removed.

NetworkingFile Access
using Sandbox;
using System;
using System.Numerics;

public class UnitStatusPoison : UnitStatus
{
	public float Damage { get; private set; }
	public float FinishDamagePercent { get; private set; }
	public float DieSpreadChance { get; private set; }
	public float RadiusMultiplier { get; private set; }
	public bool IsFlammable { get; private set; }
	public float TickTimeModifier { get; private set; }
	public int HitsToRemove { get; private set; } = 1;

	private int _hitsReceived;

	private TimeSince _timeSinceDamage;
	private const float DAMAGE_INTERVAL = 2f;

	public Enemy EnemySource { get; set; }
	public EnemyType EnemyType { get; set; }
	public Player PlayerSource { get; set; }

	private float _totalDamage;

	public UnitStatusPoison()
	{

	}

	public override void Init( Unit unit )
	{
		base.Init( unit );

		Unit.SetStatusPoison( true );

		_timeSinceDamage = Game.Random.Float( 0f, 0.25f );
	}

	public void SetValues( float damage, float finishDmgPercent, float dieSpreadChance, float radiusMultiplier, bool isFlammable, float tickSpeedModifier, int hitsToRemove )
	{
		if ( damage < Damage )
			return;

		Damage = damage;
		FinishDamagePercent = finishDmgPercent;
		DieSpreadChance = dieSpreadChance;
		RadiusMultiplier = radiusMultiplier;
		IsFlammable = isFlammable;
		TickTimeModifier = tickSpeedModifier;
		HitsToRemove = hitsToRemove;
	}

	public override void Update( float dt )
	{
		base.Update( dt );

		if ( !Unit.IsValid() )
			return;

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

		if ( _timeSinceDamage > DAMAGE_INTERVAL * TickTimeModifier )
		{
			if ( IsOnEnemy )
			{
				if ( Enemy.IsValid() )
				{
					DamageResultFlags damageFlags = DamageResultFlags.None;

					if ( PlayerSource.IsValid() )
					{
						var nearbyIncrease = PlayerSource.GetSyncStat( PlayerStat.PoisonDmgNearbyIncrease );
						if( nearbyIncrease > 0f )
						{
							var radius = PerkPoisonIncreaseNearby.RADIUS * PlayerSource.GetSyncStat( PlayerStat.RadiusMultiplier );
							if ( Enemy.WorldPosition.Distance( PlayerSource.WorldPosition ) <= radius )
							{
								damageFlags |= DamageResultFlags.PoisonIncreaseNearby;
								Damage += nearbyIncrease;
							}
						}
					}

					Enemy.DamageRpc( Damage, PlayerSource, DamageType.Poison, Enemy.WorldPosition.WithZ( 30f ), Vector2.Zero, isCrit: false, shouldFlinch: false, damageFlags );
					_totalDamage += Damage;
				}
			}
			else
			{
				if ( Player.IsValid() )
				{
					var isSelfInflicted = PlayerSource.IsValid() && PlayerSource == Player;

					var damageFlags = PlayerDamageFlags.None;
					if ( isSelfInflicted )
						damageFlags |= PlayerDamageFlags.SelfInflicted;

					Player.Damage( Damage, damageType: DamageType.Poison, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, 0f, ragdollForce: 0f, EnemySource, enemyType: EnemySource.IsValid() ? EnemySource.EnemyType : EnemyType.None, damageFlags: damageFlags );
					_totalDamage += Damage;
				}
			}

			_timeSinceDamage = 0f;
		}
	}

	public override void OnHit( float damage, Player playerSource, Enemy enemySource, DamageType damageType, bool isSelfInflicted )
	{
		base.OnHit( damage, playerSource, enemySource, damageType, isSelfInflicted );

		if ( !(damage > 0f) )
			return;

		if ( damageType == DamageType.Fire && IsFlammable )
		{
			if ( playerSource.IsValid() )
				Unit.Ignite( playerSource, enemySource: null, enemyType: EnemyType.None, damage, playerSource.GetSyncStat( PlayerStat.FireLifetime ), playerSource.GetSyncStat( PlayerStat.FireSpreadChance ), playerSource.GetSyncStat( PlayerStat.FireDmgStack ) > 0f );
			else if ( enemySource.IsValid() )
				Unit.Ignite( playerSource: null, enemySource, enemySource.EnemyType, damage, 5f, 0.25f, false );
		}

		if ( damageType != DamageType.Poison && damageType != DamageType.Self && ElapsedTime > 0.25f )
		{
			_hitsReceived++;
			if ( _hitsReceived < HitsToRemove )
				return;

			Unit.RemoveUnitStatus( this );

			if ( _totalDamage > 0f && FinishDamagePercent > 0f )
			{
				var dmg = _totalDamage * FinishDamagePercent;

				if ( IsOnEnemy )
				{
					if ( Enemy.IsValid() )
					{
						Enemy.DamageRpc( dmg, PlayerSource, DamageType.PoisonFinish, Enemy.WorldPosition.WithZ( 30f ), Vector2.Zero, isCrit: false, shouldFlinch: false );
					}
				}
				else
				{
					if ( Player.IsValid() )
					{
						var poisonIsSelfDmg = PlayerSource.IsValid() && PlayerSource == Player;

						var damageFlags = PlayerDamageFlags.None;
						if ( poisonIsSelfDmg )
							damageFlags |= PlayerDamageFlags.SelfInflicted;

						Player.Damage( dmg, damageType: DamageType.PoisonFinish, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: 0f, ragdollForce: 0f, EnemySource, EnemyType, damageFlags: damageFlags );
					}
				}

				//SS2Game.PlaySfx( "poison_tick", Unit.Position2D, pitch: Game.Random.Float( 0.8f, 0.85f ), volume: 1.1f );
			}
		}
	}

	public override void StartDying( Player player )
	{
		base.StartDying( player );

		if ( Game.Random.Float( 0f, 1f ) < DieSpreadChance )
		{
			float radius = 60f * RadiusMultiplier;
			Manager.Instance.SpawnRingRpc( Unit.Position2D, radius, new Color( 0f, 0.4f, 0f, 0.95f ), Game.Random.Float( 0.3f, 0.4f ), path: "ring2" );

			foreach ( var unit in Manager.Instance.GetNearbyUnits( Unit.Position2D, radius, except: Unit ) )
			{
				if ( !unit.IsInanimate )
					unit.Poison( PlayerSource, EnemySource, EnemyType, Damage, FinishDamagePercent, DieSpreadChance, RadiusMultiplier, IsFlammable, TickTimeModifier, HitsToRemove );
			}
		}
	}

	public override void OnRemove( bool playEffects = true )
	{
		Unit.SetStatusPoison( false );
	}

	public override void Refresh()
	{
		base.Refresh();

	}
}