unitStatus/UnitStatusFire.cs

UnitStatusFire is a UnitStatus-derived component that applies and manages a burning status on a Unit. It tracks base and stacked fire damage over time, applies periodic damage to the afflicted unit (player or enemy), handles stacking/expiration of temporary burn damage entries, and can spread fire to colliding units.

Networking
using Sandbox;
using Sandbox.UI;
using System;
using System.Numerics;

public struct StackBurnData
{
	public float damage;
	public float time;
	public float lifetime;

	public StackBurnData( float _damage, float _time, float _lifetime )
	{
		damage = _damage;
		time = _time;
		lifetime = _lifetime;
	}
}

public class UnitStatusFire : UnitStatus
{
	private TimeSince _timeSinceDamage;
	private const float DAMAGE_INTERVAL = 0.95f;

	public float SpreadChance { get; set; }
	public bool CanStack { get; set; }

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

	public float Damage { get; private set; }
	public float BaseDamage { get; private set; }

	private TimeSince _damageOtherTime;

	public bool StackDmgMode { get; set; }
	private Queue<StackBurnData> _stackTimes = new();

	public UnitStatusFire()
	{

	}

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

		Unit.SetStatusBurning( true );

		_timeSinceDamage = 0f;
	}

	public void SetStartingDamage( float damage )
	{
		BaseDamage = damage;
		Damage = damage;
	}

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

		if ( !Unit.IsValid() || Manager.Instance.IsGameOver )
			return;

		//Gizmo.Draw.Color = Color.Red;
		//Gizmo.Draw.Text( $"{Damage}\n{BaseDamage}\n{_stackTimes.Count()}", new global::Transform( Unit.WorldPosition ) );

		if ( _stackTimes.Count > 0 )
		{
			var data = _stackTimes.First();

			//DebugOverlay.Text($"{Damage}\n{string.Format("{0:0.0}", Time.Now - data.time)} / {string.Format("{0:0.0}", data.lifetime)}", Unit.Position, 0f, float.MaxValue);

			if ( Time.Now > data.time + data.lifetime )
			{
				_stackTimes.Dequeue();
				RefreshStackDmg();

				ElapsedTime = 0f;
			}
		}
		else
		{
			if ( ElapsedTime > Lifetime )
				Unit.RemoveUnitStatus( this );

			//DebugOverlay.Text($"{Damage}\n{string.Format("{0:0.0}", ElapsedTime)} / {string.Format("{0:0.0}", Lifetime)}", Unit.Position, 0f, float.MaxValue);
		}

		if ( _timeSinceDamage > DAMAGE_INTERVAL )
		{
			if( IsOnEnemy )
			{
				if ( Enemy.IsValid() )
					Enemy.DamageRpc( Damage, PlayerSource, DamageType.Fire, Enemy.WorldPosition.WithZ( 30f ), Vector2.Zero, isCrit: false, shouldFlinch: false );
			}
			else
			{
				if( Player.IsValid() )
				{
					var isSelfInflicted = PlayerSource.IsValid() && PlayerSource == Player;

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

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

			_timeSinceDamage = 0f;
		}
	}

	public void AddDamageStack( float damage, float lifetime )
	{
		_stackTimes.Enqueue( new StackBurnData( damage, Time.Now, lifetime ) );
		RefreshStackDmg();
	}

	void RefreshStackDmg()
	{
		Damage = BaseDamage;

		foreach ( var data in _stackTimes )
			Damage += data.damage;

		//Damage = PlayerSource.Stats[PlayerStat.FireDamage];

		//foreach ( var data in _stackTimes )
		//	Damage += data.damage;

		//Damage *= PlayerSource.GetDamageMultiplier();

		// todo: change particle appearance as stacks increase
	}

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

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

		ElapsedTime = 0f;
	}

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

		if ( Manager.Instance.IsGameOver )
			return;

		if ( _damageOtherTime > DAMAGE_INTERVAL )
		{
			if ( other is Unit unit && !unit.IsDying && !unit.IsBurning && !unit.IsInTheAir )
			{
				Vector2 dir = (unit.Position2D - Unit.Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR
					? (unit.Position2D - Unit.Position2D).Normal
					: Utils.GetRandomVector();

				var hitPos = unit.Position2D - dir * unit.Radius;

				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 )
				{
					var isSelfInflicted = PlayerSource.IsValid() && PlayerSource == player;

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

					player.DamageRpc( Damage, damageType: DamageType.Fire, hitPos, dir, upwardAmount: 0f, force: 0f, ragdollForce: 0.1f, EnemySource, EnemyType, damageFlags: damageFlags );
				}

				if ( Game.Random.Float( 0f, 1f ) < SpreadChance )
					unit.Ignite( PlayerSource, EnemySource, EnemyType, Damage, Lifetime, SpreadChance, CanStack );

				_damageOtherTime = 0f;
			}
		}
	}
}