Conveyance/Windmilltuner.cs
using Sandbox;

/// <summary>
/// Tunes a HingeJoint to behave like a continuously spinning windmill / fan / hazard blade.
/// Unlike the seesaw, this contraption has no rest pose — it spins forever in one direction
/// at a fixed speed. The plank-of-doom (or fan blade, or rotating platform) is just whatever
/// model you attach to the Rigidbody this joint belongs to.
///
/// Place this on the SAME GameObject as the HingeJoint (i.e. the child of the spinner body).
///
/// IMPORTANT: this script does NOT change the HingeJoint's Motor mode. Set it once in the
/// editor to "Target Velocity" and leave it. The script only writes the numeric properties.
///
/// Tuning intuition:
///   - SpinSpeed 60–120 deg/sec: lazy windmill, easy to walk past, atmospheric.
///   - SpinSpeed 180–360 deg/sec: active hazard, requires timing.
///   - SpinSpeed 720+: comedy-fast, will fling boxes across the map. Funny once.
///   - MaxTorque low (100–1000): a box wedged in the blades can halt the windmill. Players
///     can sabotage it. Surprisingly fun emergent behaviour.
///   - MaxTorque high (10000+): blades plough through everything, including the player.
///     Boxes get yeeted. The testbed default.
///
/// The "stops on collision" failure mode at low MaxTorque is actually a feature — pair it
/// with a respawning box dispenser and you've got a sabotage minigame for free.
/// </summary>
public sealed class WindmillTuner : Component
{
	/// <summary>
	/// The HingeJoint this tuner controls. Defaults to one on the same GameObject if not set.
	/// </summary>
	[Property] public HingeJoint Hinge { get; set; }

	/// <summary>
	/// Rotation speed in degrees per second. Positive = one direction, ReverseDirection flips
	/// it. The underlying joint property is in degrees/sec.
	/// </summary>
	[Property, Range( 0, 1080 )] public float SpinSpeed { get; set; } = 180f;

	/// <summary>
	/// Flips the spin direction without having to remember which sign is which for this
	/// particular joint axis.
	/// </summary>
	[Property] public bool ReverseDirection { get; set; } = false;

	/// <summary>
	/// How much torque the motor can apply to maintain its target velocity. Low values mean
	/// the windmill can be stopped by colliding with heavy objects (or wedged boxes); high
	/// values mean it relentlessly bulldozes through everything.
	/// </summary>
	[Property, Range( 0, 50000 )] public float MaxTorque { get; set; } = 5000f;

	/// <summary>
	/// Resistance to motion regardless of motor input. Usually 0 for a windmill — friction
	/// fights the motor and just wastes torque. Raise it if you want a windmill that takes
	/// a moment to spin up after being stopped, but it's an unusual effect.
	/// </summary>
	[Property, Range( 0, 10 )] public float Friction { get; set; } = 0f;

	/// <summary>
	/// Motor responsiveness — how aggressively it tries to reach TargetVelocity. Higher =
	/// snappier acceleration, but combined with high MaxTorque can cause solver jitter.
	/// The testbed uses 1, which is a sensible default.
	/// </summary>
	[Property, Range( 0.1f, 10f )] public float Responsiveness { get; set; } = 1f;

	/// <summary>
	/// Damping ratio of the motor. 1 = critically damped, almost always what you want for
	/// a velocity-driven motor — anything lower can cause speed oscillation around the target.
	/// </summary>
	[Property, Range( 0.1f, 2f )] public float DampingRatio { get; set; } = 1f;

	protected override void OnStart()
	{
		// Auto-find the hinge on the same GameObject if the user didn't drag one in.
		if ( Hinge is null )
			Hinge = GetComponent<HingeJoint>();

		Apply();
	}

	/// <summary>
	/// Pushes all tuner values onto the underlying HingeJoint.
	/// </summary>
	[Button( "Apply Now" )]
	public void Apply()
	{
		if ( Hinge is null )
		{
			Log.Warning( $"{nameof( WindmillTuner )} on {GameObject.Name}: no HingeJoint assigned or found." );
			return;
		}

		// We deliberately don't write Hinge.Motor — set it to "Target Velocity" in the editor
		// once and leave it. Same trick as SeesawTuner: avoid the MotorMode enum namespace issue
		// by only touching numeric properties.
		Hinge.TargetVelocity = ReverseDirection ? -SpinSpeed : SpinSpeed;
		Hinge.MaxTorque = MaxTorque;
		Hinge.Friction = Friction;
		Hinge.Frequency = Responsiveness;
		Hinge.DampingRatio = DampingRatio;

		// Angle limits would cap the windmill at a fixed arc. We explicitly zero them so the
		// blades can spin freely. (If the user wants an oscillating wiper-blade contraption,
		// they can set these manually after Apply — but it's not the windmill use case.)
		Hinge.MinAngle = 0f;
		Hinge.MaxAngle = 0f;
	}
}