MouseTargeting.cs
using System;
using Sandbox;
using Sandbox.Citizen;
/// <summary>
/// Two-axis hand positioning (vertical/depth) with an elliptical reach
/// limit, plus PC interference noise that RoundManager drives.
/// </summary>
public sealed class MouseTargeting : Component
{
[Property] public GameObject HandTarget { get; set; }
[Property] public GameObject ChestAnchor { get; set; }
[Property] public float VerticalMin { get; set; } = -30.0f;
[Property] public float VerticalMax { get; set; } = 10.0f;
[Property] public float DepthMin { get; set; } = -20.0f;
[Property] public float DepthMax { get; set; } = 50.0f;
[Property] public float Sensitivity { get; set; } = 0.1f;
[Property] public float ShoulderHeightOffset { get; set; } = 55.0f;
[Property] public float SideOffset { get; set; } = 0.0f;
// Freeze is now driven by RoundManager (between-round reset), not a
// keybind - set this from code, not the player.
[Property] public bool FreezePosition { get; set; } = false;
// PC interference, set by RoundManager each round.
public float InterferenceIntensity { get; set; } = 0.0f;
// STRENGTH = raw magnitude of the drift.
[Property] public float NoiseStrength { get; set; } = 40.0f;
// AGGRESSIVENESS = how often/fast the drift changes direction.
// Low = slow, sweeping drift. High = twitchy, jittery drift.
[Property] public float NoiseAggressiveness { get; set; } = 2.0f;
private CitizenAnimationHelper _anim;
private float _verticalOffset;
private float _depthOffset;
// Smoothed noise state
private float _noiseV, _noiseD;
private float _noiseTargetV, _noiseTargetD;
private float _noiseTimer;
protected override void OnStart()
{
_anim = Components.Get<CitizenAnimationHelper>();
if ( _anim is null )
{
Log.Warning( "MouseTargeting: no CitizenAnimationHelper found on this GameObject." );
return;
}
_anim.IsSitting = true;
_anim.IkRightHand = HandTarget;
ResetToHome();
}
// Called by RoundManager between rounds to snap back to a neutral pose.
public void ResetToHome()
{
_verticalOffset = VerticalMin;
_depthOffset = DepthMax;
_noiseV = 0;
_noiseD = 0;
}
protected override void OnUpdate()
{
if ( HandTarget is null || ChestAnchor is null || FreezePosition )
return;
_verticalOffset -= Input.MouseDelta.y * Sensitivity;
_depthOffset += Input.MouseDelta.x * Sensitivity;
if ( InterferenceIntensity > 0 )
{
_noiseTimer += Time.Delta;
var retargetInterval = 1f / MathF.Max( 0.01f, NoiseAggressiveness );
if ( _noiseTimer >= retargetInterval )
{
_noiseTimer = 0;
_noiseTargetV = Game.Random.Float( -1f, 1f );
_noiseTargetD = Game.Random.Float( -1f, 1f );
}
_noiseV = MathX.Lerp( _noiseV, _noiseTargetV, Time.Delta * NoiseAggressiveness );
_noiseD = MathX.Lerp( _noiseD, _noiseTargetD, Time.Delta * NoiseAggressiveness );
_verticalOffset += _noiseV * InterferenceIntensity * NoiseStrength * Time.Delta;
_depthOffset += _noiseD * InterferenceIntensity * NoiseStrength * Time.Delta;
}
var centerV = (VerticalMin + VerticalMax) * 0.5f;
var halfV = (VerticalMax - VerticalMin) * 0.5f;
var centerD = (DepthMin + DepthMax) * 0.5f;
var halfD = (DepthMax - DepthMin) * 0.5f;
var normV = (_verticalOffset - centerV) / halfV;
var normD = (_depthOffset - centerD) / halfD;
var dist = MathF.Sqrt( normV * normV + normD * normD );
if ( dist > 1.0f )
{
normV /= dist;
normD /= dist;
}
_verticalOffset = centerV + normV * halfV;
_depthOffset = centerD + normD * halfD;
var rot = ChestAnchor.Transform.Rotation;
var shoulderPos = ChestAnchor.Transform.Position + rot.Up * ShoulderHeightOffset;
var worldPos = shoulderPos
+ rot.Forward * _depthOffset
+ rot.Up * _verticalOffset
+ rot.Right * SideOffset;
HandTarget.Transform.Position = worldPos;
}
}