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;
}
}