CupTiltController.cs
using System;
using Sandbox;
/// <summary>
/// Momentum-based wrist tilt. Left/right click accelerate opposite
/// directions, drag bleeds velocity off over time, plus PC interference
/// noise that RoundManager drives.
/// </summary>
public sealed class CupTiltController : Component
{
[Property] public GameObject HandTarget { get; set; }
[Property] public GameObject ChestAnchor { get; set; }
[Property] public bool InvertDirection { get; set; } = false;
// Freeze is now driven by RoundManager, not a keybind.
[Property] public bool FreezeTilt { get; set; } = false;
[Property] public float TiltAccel { get; set; } = 100.0f;
[Property] public float Drag { get; set; } = 5.0f;
[Property] public float MinTilt { get; set; } = -100.0f;
[Property] public float MaxTilt { get; set; } = 180.0f;
// PC interference, set by RoundManager each round.
public float InterferenceIntensity { get; set; } = 0.0f;
// STRENGTH = raw magnitude of the random velocity kicks.
[Property] public float NoiseStrength { get; set; } = 30.0f;
// AGGRESSIVENESS = how often/fast the kicks change direction.
[Property] public float NoiseAggressiveness { get; set; } = 2.0f;
private float _tiltVelocity;
private float _tiltAngle;
private Rotation _baseRotation;
private float _noiseCurrent, _noiseTarget, _noiseTimer;
public float TiltAngle => _tiltAngle;
protected override void OnStart()
{
if ( HandTarget is not null )
_baseRotation = HandTarget.Transform.Rotation;
ResetToHome();
}
// Called by RoundManager between rounds to snap back to upright.
public void ResetToHome()
{
_tiltAngle = 0;
_tiltVelocity = 0;
_noiseCurrent = 0;
_noiseTarget = 0;
}
protected override void OnUpdate()
{
if ( HandTarget is null || ChestAnchor is null )
return;
if ( !FreezeTilt )
{
if ( Input.Down( "attack1" ) )
_tiltVelocity += TiltAccel * Time.Delta;
if ( Input.Down( "attack2" ) )
_tiltVelocity -= TiltAccel * Time.Delta;
if ( InterferenceIntensity > 0 )
{
_noiseTimer += Time.Delta;
var retargetInterval = 1f / MathF.Max( 0.01f, NoiseAggressiveness );
if ( _noiseTimer >= retargetInterval )
{
_noiseTimer = 0;
_noiseTarget = Game.Random.Float( -1f, 1f );
}
_noiseCurrent = MathX.Lerp( _noiseCurrent, _noiseTarget, Time.Delta * NoiseAggressiveness );
_tiltVelocity += _noiseCurrent * InterferenceIntensity * NoiseStrength * Time.Delta;
}
if ( _tiltVelocity > 0 )
_tiltVelocity = MathF.Max( 0, _tiltVelocity - Drag * Time.Delta );
else if ( _tiltVelocity < 0 )
_tiltVelocity = MathF.Min( 0, _tiltVelocity + Drag * Time.Delta );
_tiltAngle += _tiltVelocity * Time.Delta;
if ( _tiltAngle > MaxTilt )
{
_tiltAngle = MaxTilt;
_tiltVelocity = 0;
}
else if ( _tiltAngle < MinTilt )
{
_tiltAngle = MinTilt;
_tiltVelocity = 0;
}
}
var tiltAngleApplied = InvertDirection ? -_tiltAngle : _tiltAngle;
var tiltAxis = ChestAnchor.Transform.Rotation.Right;
var tiltRotation = Rotation.FromAxis( tiltAxis, tiltAngleApplied );
HandTarget.Transform.Rotation = tiltRotation * _baseRotation;
}
}