Weapons/ToolGun/Modes/Hydraulic/BaseLengthConstraintTool.cs
/// <summary>
/// Shared geometry for two-point length-changing constraints (Hydraulic, Muscle, Winch):
/// a shaft between two capped anchors, driven by a <see cref="SliderJoint"/>.
/// </summary>
public abstract class BaseLengthConstraintTool : BaseConstraintToolMode
{
	protected abstract string CapModelA { get; }
	protected abstract string CapModelB { get; }
	protected abstract string ShaftMaterial { get; }
	protected abstract string UndoName { get; }

	protected abstract LengthConstraintEntity.ConstraintKind Kind { get; }

	protected virtual float ShaftWidth => 2f;
	protected virtual float StartLength => 0.5f;
	protected virtual float MinLength => 5.0f;
	protected virtual float MaxLengthMultiplier => 2.0f;
	protected virtual float PushSpeed => 0.25f;
	protected virtual float PullSpeed => 0.25f;
	protected virtual bool AnimatedByDefault => false;

	protected override IEnumerable<GameObject> FindConstraints( GameObject linked, GameObject target )
	{
		foreach ( var cleanup in linked.GetComponentsInChildren<ConstraintCleanup>( true ) )
		{
			if ( linked != target && cleanup.Attachment?.Root != target ) continue;
			if ( cleanup.GameObject.GetComponentInChildren<LengthConstraintEntity>() is not null )
				yield return cleanup.GameObject;
		}
	}

	protected override void CreateConstraint( SelectionPoint point1, SelectionPoint point2 )
	{
		if ( point1.GameObject == point2.GameObject )
			return;

		var line = point1.WorldPosition() - point2.WorldPosition();
		var baseName = UndoName.ToLowerInvariant();

		var go1 = new GameObject( false, $"{baseName}_a" );
		go1.Parent = point1.GameObject;
		go1.LocalTransform = point1.LocalTransform;
		go1.WorldRotation = Rotation.LookAt( -line );
		go1.Tags.Add( "constraint" );

		var go2 = new GameObject( false, $"{baseName}_b" );
		go2.Parent = point2.GameObject;
		go2.LocalTransform = point2.LocalTransform;
		go2.WorldRotation = Rotation.LookAt( -line );
		go2.Tags.Add( "constraint" );

		var cleanup = go1.AddComponent<ConstraintCleanup>();
		cleanup.Attachment = go2;

		var len = (point1.WorldPosition() - point2.WorldPosition()).Length;

		// End caps
		var capA = new GameObject( go1, true, $"{baseName}_cap_a" );
		capA.LocalPosition = Vector3.Zero;
		capA.WorldRotation = Rotation.LookAt( line ) * Rotation.FromPitch( -90f );
		capA.AddComponent<ModelRenderer>().Model = Model.Load( CapModelA );

		var capB = new GameObject( go2, true, $"{baseName}_cap_b" );
		capB.LocalPosition = Vector3.Zero;
		capB.WorldRotation = Rotation.LookAt( -line ) * Rotation.FromPitch( -90f );
		capB.AddComponent<ModelRenderer>().Model = Model.Load( CapModelB );

		// Shaft, using line renderer
		var lineRenderer = go1.AddComponent<LineRenderer>();
		lineRenderer.Points = [go1, go2];
		lineRenderer.Face = SceneLineObject.FaceMode.Cylinder;
		lineRenderer.Texturing = lineRenderer.Texturing with { Material = Material.Load( ShaftMaterial ), WorldSpace = true, UnitsPerTexture = 32 };
		lineRenderer.Lighting = true;
		lineRenderer.CastShadows = true;
		lineRenderer.Width = ShaftWidth;
		lineRenderer.Color = Color.White;

		SliderJoint joint;

		var jointGo = new GameObject( go1, true, baseName );

		// Joint
		{
			joint = jointGo.AddComponent<SliderJoint>();
			joint.Attachment = Sandbox.Joint.AttachmentMode.Auto;
			joint.Body = go2;
			joint.MinLength = len;
			joint.MaxLength = len;
			joint.EnableCollision = true;
		}

		var entity = jointGo.AddComponent<LengthConstraintEntity>();
		entity.Kind = Kind;
		entity.Length = StartLength;
		entity.MinLength = MinLength;
		entity.MaxLength = len * MaxLengthMultiplier;
		entity.PushSpeed = PushSpeed;
		entity.PullSpeed = PullSpeed;
		entity.Animated = AnimatedByDefault;
		entity.Joint = joint;

		jointGo.AddComponent<CapsuleCollider>();

		// The joint is the part with a collider, so it's what the remover tool hits. Tag it removable
		// and tie it to the anchors, so removing the shaft takes the caps and anchors with it (and the
		// joint goes when its anchor does).
		jointGo.Tags.Add( "removable" );
		jointGo.Tags.Add( "constraint" );
		jointGo.AddComponent<ConstraintCleanup>().Attachment = go1;

		go2.NetworkSpawn( true, null );
		go1.NetworkSpawn( true, null );
		jointGo.NetworkSpawn( true, null );

		Track( go1, go2, jointGo );

		var undo = Player.Undo.Create();
		undo.Name = UndoName;
		undo.Add( go1 );
		undo.Add( go2 );
		undo.Add( jointGo );
	}

}