unitStatus/UnitStatusChained.cs

A unit status component that manages chained constraints for a Unit. It stores ChainedData entries, syncs them via RPCs on the Unit, updates lifetimes, enforces chain distance by applying a velocity impulse, and removes expired or invalid chains.

Networking
using System;
using Sandbox;

public class ChainedData
{
	public int Id { get; set; }
	public bool AnchoredToUnit { get; set; }
	public Unit AnchorUnit { get; set; } // the unit that this unit is chained to, if any
	public Vector2 ChainPos { get; set; }
	public float ChainLength { get; set; }
	public float Lifetime { get; set; }
}

public class UnitStatusChained : UnitStatus
{
	private List<ChainedData> _chainDatas = new();

	private int _chainIndex;

	public UnitStatusChained()
	{

	}

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

		Unit.SetStatusChained( true );
	}

	public void AddChain( Unit anchorUnit, Vector2 chainPos, float chainLength, float lifetime )
	{
		if ( anchorUnit.IsValid() )
		{
			ChainedData existingChain = null;

			foreach ( var chain in _chainDatas )
			{
				if ( chain.AnchorUnit == anchorUnit )
				{
					existingChain = chain;
					break;
				}
			}

			if ( existingChain != null )
			{
				//existingChain.ChainPos = chainPos;
				//existingChain.ChainLength = chainLength;
				//existingChain.Lifetime = Lifetime;
				return;
			}
		}

		_chainDatas.Add( new ChainedData()
		{
			Id = _chainIndex,
			AnchoredToUnit = anchorUnit.IsValid(),
			AnchorUnit = anchorUnit,
			ChainPos = chainPos,
			ChainLength = chainLength,
			Lifetime = lifetime
		} );

		Unit.AddChainRpc( _chainIndex, lifetime, chainLength );

		Unit.SetChainAnchorPosRpc( _chainIndex, chainPos );

		if( anchorUnit.IsValid() )
			Unit.SetChainAnchorUnitRpc( _chainIndex, anchorUnit );

		_chainIndex++;
	}

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

		if ( !Unit.IsValid() )
			return;

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

		for ( int i = _chainDatas.Count - 1; i >= 0; i-- )
		{
			var chainData = _chainDatas[i];

			chainData.Lifetime -= dt;
			if ( chainData.Lifetime < 0f || ( chainData.AnchoredToUnit && !chainData.AnchorUnit.IsValid() ) )
			{
				_chainDatas.RemoveAt( i );
				Unit.RemoveChainRpc( chainData.Id );
			}
			else
			{
				if ( chainData.AnchorUnit.IsValid() )
					chainData.ChainPos = chainData.AnchorUnit.Position2D;

				var lengthSqr = (Unit.Position2D - chainData.ChainPos).LengthSquared;
				if ( lengthSqr > MathF.Pow( chainData.ChainLength, 2f ) )
				{
					Unit.Velocity += (chainData.ChainPos - Unit.Position2D).Normal * (lengthSqr - MathF.Pow( chainData.ChainLength, 2f )) * 0.1f * Time.Delta;
				}
			}
		}

		//Gizmo.Draw.Color = Color.White.WithAlpha( 0.2f );
		//Gizmo.Draw.LineCircle(
		//	center: (Vector3)ChainPos + Vector3.Up * 1f,
		//	forward: Vector3.Up,
		//	radius: ChainLength,
		//	startAngle: Time.Now * -150f,
		//	totalDegrees: 360f,
		//	sections: 50
		//);

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Line(
		//	(Vector3)ChainPos + Vector3.Up * 1f,
		//	(Vector3)Unit.Position2D + Vector3.Up * 1f
		//);
	}

	public override void OnRemove( bool playEffects = true )
	{
		foreach ( var chainData in _chainDatas )
			Unit.RemoveChainRpc( chainData.Id );

		Unit.SetStatusChained( false );
	}

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

	}
}