Weapons/ToolGun/Modes/Wheel/WheelTool.cs
using Sandbox.UI;

[Hide]
[Title( "#tool.name.wheel" )]
[Icon( "🛞" )]
[ClassName( "wheeltool" )]
[Group( "#tool.group.building" )]
public sealed class WheelTool : ToolMode
{
	public override bool UseSnapGrid => true;
	public override IEnumerable<string> TraceIgnoreTags => ["constraint", "collision"];
	[Property, ResourceSelect( Extension = "wdef", AllowPackages = true ), Title( "Wheel" )]
	public string Definition { get; set; } = "entities/wheel/basic.wdef";

	public override string Description => "#tool.hint.wheeltool.description";

	Vector3 _axis = Vector3.Right;
	bool _reversed = false;

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

		RegisterAction( ToolInput.Primary, () => "#tool.hint.wheeltool.place", OnPlace );
		RegisterAction( ToolInput.Secondary, () => "#tool.hint.wheeltool.toggle_axis", OnToggleAxis );
		RegisterAction( ToolInput.Reload, () => "#tool.hint.wheeltool.toggle_direction", OnToggleDirection );
	}

	void OnToggleAxis()
	{
		_axis = _axis == Vector3.Right ? Vector3.Up : Vector3.Right;
	}

	void OnToggleDirection()
	{
		_reversed = !_reversed;
	}

	void OnPlace()
	{
		var select = TraceSelect();
		if ( !select.IsValid() ) return;

		var def = ResourceLibrary.Get<WheelDefinition>( Definition );
		if ( def == null || def.Prefab?.GetScene() is not Scene scene ) return;

		var placementTrans = GetPlacementTransform( select, scene );
		SpawnWheel( select, def, placementTrans, _reversed );
		ShootEffects( select );
	}

	Transform GetPlacementTransform( SelectionPoint select, Scene scene )
	{
		var pos = select.WorldTransform();
		var modelBounds = scene.GetBounds();
		var surfaceOffset = modelBounds.Size.y * 0.5f;

		var placementTrans = new Transform( pos.Position + pos.Rotation.Forward * surfaceOffset );
		placementTrans.Rotation = Rotation.LookAt( pos.Rotation.Forward, pos.Rotation * _axis ) * new Angles( 0, 90, 0 );
		placementTrans.Scale = scene.LocalScale;
		return placementTrans;
	}

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

		var select = TraceSelect();
		if ( !select.IsValid() ) return;

		var def = ResourceLibrary.Get<WheelDefinition>( Definition );
		if ( def == null || def.Prefab?.GetScene() is not Scene scene ) return;

		var placementTrans = GetPlacementTransform( select, scene );
		var modelBounds = scene.GetBounds();

		DebugOverlay.GameObject( scene, transform: placementTrans, castShadows: true, color: Color.White.WithAlpha( 0.9f ) );

		var steerAngle = MathF.Sin( Time.Now * 2f ) * 25f;
		var steer = Rotation.FromAxis( placementTrans.Forward, steerAngle );
		var cylRadius = MathF.Max( modelBounds.Size.x, modelBounds.Size.z ) * 0.5f * placementTrans.Scale.x + 0.5f;
		var cylAxis = steer * placementTrans.Right;
		DebugOverlay.Cylinder( new Capsule( placementTrans.Position - cylAxis * 2f, placementTrans.Position + cylAxis * 2f, cylRadius ), Color.Yellow, segments: 32 );

		var overlayRadius = MathF.Max( modelBounds.Size.x, modelBounds.Size.z ) * 0.01f * placementTrans.Scale.x;
		WheelOverlay.DrawDirection( placementTrans.Position, placementTrans.Right, select.WorldTransform().Rotation.Up, overlayRadius, _reversed );
	}

	[Rpc.Host]
	public void SpawnWheel( SelectionPoint point, WheelDefinition def, Transform tx, bool reversed )
	{
		if ( def == null || def.Prefab?.GetScene() is not Scene scene ) return;

		var wheelGo = scene.Clone( new CloneConfig { StartEnabled = false } );
		wheelGo.Name = "wheel";
		wheelGo.Tags.Add( "removable" );
		wheelGo.Tags.Add( "constraint" );
		wheelGo.WorldTransform = tx;

		var we = wheelGo.GetOrAddComponent<WheelEntity>();
		we.Reversed = reversed;
		var joint = wheelGo.GetComponentInChildren<WheelJoint>( true );

		if ( joint is null )
		{
			var wheelAnchor = new GameObject( true, "anchor2" );
			wheelAnchor.Parent = wheelGo;
			wheelAnchor.LocalRotation = new Angles( 0, 90, 90 );

			//var joint = jointGo.AddComponent<HingeJoint>();
			joint = wheelAnchor.AddComponent<WheelJoint>();
			joint.Attachment = Joint.AttachmentMode.Auto;
			joint.EnableSuspension = true;
			joint.EnableSuspensionLimit = true;
			joint.SuspensionLimits = new Vector2( -32, 32 );
			joint.EnableCollision = false;
		}

		ApplyPhysicsProperties( wheelGo );

		joint.Body = point.GameObject;

		wheelGo.NetworkSpawn( true, null );

		Track( wheelGo );

		var undo = Player.Undo.Create();
		undo.Name = "Wheel";
		undo.Add( wheelGo );

		CheckContraptionStats( point.GameObject );
	}
}