OneMoreRoundWeapon.Accuracy.cs
using Sandbox;
using System;
[Flags]
public enum WeaponAccuracyFlags
{
None = 0,
Moving = 1 << 0,
Crouched = 1 << 1,
Airborne = 1 << 2,
Sliding = 1 << 3
}
/// <summary>
/// Accuracy state and spread resolution for OneMoreRoundWeapon.
///
/// The default LegacyThreshold model intentionally preserves the behavior used
/// by the existing OMR arsenal. ContinuousSpeed is available as the scalable
/// replacement, but does nothing until a weapon definition explicitly selects
/// it. This lets architecture move forward without sneaking a balance pass into
/// the refactor.
/// </summary>
public partial class OneMoreRoundWeapon
{
// =========================================================
// AUTHORED ACCURACY
// =========================================================
[Property, Group( "Accuracy" )]
public OMRMovementAccuracyModel MovementAccuracyModel { get; set; } =
OMRMovementAccuracyModel.LegacyThreshold;
/// <summary>
/// Legacy movement threshold in world-units per second.
/// Used only by LegacyThreshold.
/// </summary>
[Property, Group( "Accuracy" )]
public float MovingSpeedThreshold { get; set; } = 25f;
/// <summary>
/// ContinuousSpeed begins adding the moving penalty at this normalized
/// fraction of the locomotion provider's reference move speed.
/// </summary>
[Property, Group( "Accuracy" )]
[Range( 0f, 1f )]
public float MovementAccuracyStartFraction { get; set; } = 0.10f;
/// <summary>
/// ContinuousSpeed reaches the full MovingSpreadMultiplier at this normalized
/// fraction of reference move speed.
/// </summary>
[Property, Group( "Accuracy" )]
[Range( 0f, 1f )]
public float MovementAccuracyFullFraction { get; set; } = 1.0f;
[Property, Group( "Accuracy" )]
[Range( 1f, 6f )]
public float MovingSpreadMultiplier { get; set; } = 1.30f;
[Property, Group( "Accuracy" )]
[Range( 0.1f, 2f )]
public float CrouchedSpreadMultiplier { get; set; } = 0.85f;
[Property, Group( "Accuracy" )]
[Range( 1f, 10f )]
public float AirborneSpreadMultiplier { get; set; } = 2.20f;
[Property, Group( "Accuracy" )]
[Range( 1f, 10f )]
public float SlidingSpreadMultiplier { get; set; } = 1.60f;
[Property, Group( "Accuracy" )]
[Range( 0f, 1f )]
public float AimSpreadMultiplier { get; set; } = 0.30f;
// =========================================================
// RUNTIME ACCURACY STATE
// =========================================================
[Sync]
public WeaponAccuracyFlags AccuracyFlags { get; set; }
/// <summary>
/// 0 = settled movement contribution, 1 = full moving penalty.
/// In LegacyThreshold this is exactly 0 or 1. In ContinuousSpeed it blends.
/// </summary>
public float CurrentMovementAccuracyBlend { get; private set; }
/// <summary>
/// Effective multiplier currently applied on top of BaseCombatWeapon spread.
/// Useful to HUD/debug code without reimplementing the rules.
/// </summary>
public float CurrentAccuracyMultiplier => ResolveAccuracyMultiplier();
public override Vector2 CurrentSpread
{
get
{
if (
TryGetTemporarySpreadOverride(
out Vector2 spreadOverride
)
)
{
return spreadOverride;
}
return
base.CurrentSpread *
ResolveAccuracyMultiplier();
}
}
// =========================================================
// STATE COLLECTION
// =========================================================
private void UpdateAccuracyState()
{
if (
!TryGetLocomotionSnapshot(
out WeaponLocomotionSnapshot locomotion
)
)
{
CurrentMovementAccuracyBlend = 0f;
SetAccuracyFlags( WeaponAccuracyFlags.None );
return;
}
float movementBlend = ResolveMovementAccuracyBlend( locomotion );
CurrentMovementAccuracyBlend = movementBlend;
WeaponAccuracyFlags nextFlags =
WeaponAccuracyFlags.None;
if ( movementBlend > 0f )
{
nextFlags |= WeaponAccuracyFlags.Moving;
}
if ( locomotion.IsCrouched )
{
nextFlags |= WeaponAccuracyFlags.Crouched;
}
if ( locomotion.IsAirborne )
{
nextFlags |= WeaponAccuracyFlags.Airborne;
}
if ( locomotion.IsSliding )
{
nextFlags |= WeaponAccuracyFlags.Sliding;
}
SetAccuracyFlags( nextFlags );
}
private float ResolveMovementAccuracyBlend(
WeaponLocomotionSnapshot locomotion
)
{
if (
MovementAccuracyModel ==
OMRMovementAccuracyModel.LegacyThreshold
)
{
return locomotion.HorizontalSpeed >= MovingSpeedThreshold
? 1f
: 0f;
}
float start = MathX.Clamp(
MovementAccuracyStartFraction,
0f,
1f
);
float full = MathX.Clamp(
MovementAccuracyFullFraction,
start,
1f
);
float speed = MathX.Clamp(
locomotion.NormalizedMoveSpeed,
0f,
1f
);
if ( speed <= start )
return 0f;
if ( full <= start + 0.0001f )
return 1f;
return MathX.Clamp(
(speed - start) /
(full - start),
0f,
1f
);
}
private void SetAccuracyFlags(
WeaponAccuracyFlags flags
)
{
if ( AccuracyFlags == flags )
return;
AccuracyFlags = flags;
}
private void ResetAccuracyState()
{
AccuracyFlags = WeaponAccuracyFlags.None;
CurrentMovementAccuracyBlend = 0f;
ResetLocomotionProviderCache();
}
// =========================================================
// SPREAD RESOLUTION
// =========================================================
private float ResolveAccuracyMultiplier()
{
float multiplier = 1f;
bool sliding =
AccuracyFlags.HasFlag( WeaponAccuracyFlags.Sliding );
bool airborne =
AccuracyFlags.HasFlag( WeaponAccuracyFlags.Airborne );
bool moving =
AccuracyFlags.HasFlag( WeaponAccuracyFlags.Moving );
bool crouched =
AccuracyFlags.HasFlag( WeaponAccuracyFlags.Crouched );
if ( sliding )
{
multiplier *= SlidingSpreadMultiplier;
}
else if ( airborne )
{
multiplier *= AirborneSpreadMultiplier;
}
else
{
if ( moving )
{
if (
MovementAccuracyModel ==
OMRMovementAccuracyModel.ContinuousSpeed
)
{
multiplier *= MathX.Lerp(
1f,
MovingSpreadMultiplier,
CurrentMovementAccuracyBlend
);
}
else
{
multiplier *= MovingSpreadMultiplier;
}
}
if ( crouched )
{
multiplier *= CrouchedSpreadMultiplier;
}
}
float aimBlend = AimAccuracyBlend;
if ( aimBlend > 0f )
{
multiplier *= MathX.Lerp(
1f,
AimSpreadMultiplier,
aimBlend
);
}
// Zero is intentionally valid for precision weapons at full ADS. Existing
// weapons without an optic profile keep their old immediate ADS multiplier.
// definitions use non-zero values, so this changes no current gun feel.
return MathF.Max( multiplier, 0f );
}
}