OneMoreRoundWeapon.Optics.cs
using Sandbox;
using System;
/// <summary>
/// Generic optic/ADS capability state. This owns no scope rendering; it exposes
/// a stable mechanical contract that a simple overlay or future PiP renderer can
/// consume without changing the weapon core again.
/// </summary>
public partial class OneMoreRoundWeapon : IWeaponOpticState
{
[Property, Group( "Optics" ), Description( "Optional reusable optic profile. When a Weapon Definition is assigned, its profile becomes canonical and overwrites this legacy/prefab field during initialization." )]
public OMROpticProfile OpticProfile { get; set; }
public bool HasOpticProfile =>
OpticProfile is not null &&
OpticProfile.IsValid;
public OMROpticProfile ActiveOpticProfile =>
HasOpticProfile ? OpticProfile : null;
/// <summary>
/// 0 = hip, 1 = fully aimed. Unlike IsAiming this reflects the authored ADS
/// transition and is therefore suitable for precision/scope thresholds.
/// </summary>
public float AimFraction =>
MathX.Clamp( _aimFovBlend, 0f, 1f );
/// <summary>
/// Accuracy contribution of ADS. Existing weapons without an optic profile
/// preserve the original immediate IsAiming behavior exactly.
/// </summary>
public float AimAccuracyBlend =>
ResolveAimAccuracyBlend();
/// <summary>
/// Presentation threshold for magnified optics. Phase 5C/5D can consume this
/// to show a simple scope or PiP lens without duplicating ADS timing logic.
/// </summary>
public bool IsScopePresentationActive
{
get
{
if ( !HasOpticProfile )
return false;
if ( OpticProfile.Mode != OMROpticMode.MagnifiedScope )
return false;
float threshold = MathX.Clamp(
OpticProfile.ScopePresentationThreshold,
0f,
1f
);
return IsAiming && AimFraction >= threshold;
}
}
/// <summary>
/// Current additional sensitivity contribution from this optic. FOV-matched
/// sensitivity remains owned by PlayerCameraFeel; this scale composes on top.
/// </summary>
public float CurrentOpticSensitivityScale
{
get
{
if ( !HasOpticProfile )
return 1f;
float fullScale = MathX.Clamp(
OpticProfile.ScopedSensitivityMultiplier,
0.1f,
2f
);
return MathX.Lerp(
1f,
fullScale,
AimFraction
);
}
}
/// <summary>
/// Presentation-only FOV scale for the ordinary gameplay/peripheral camera
/// while a PiP scope is aimed. Non-PiP optics keep their existing AimFovScale
/// path and therefore always report 1 here.
/// </summary>
public float CurrentPeripheralAimFovScale
{
get
{
if ( !UsesPictureInPictureScope )
return 1f;
float fullScale = MathX.Clamp(
OpticProfile.PeripheralAimFovScale,
0.5f,
1.5f
);
return MathX.Lerp(
1f,
fullScale,
AimFraction
);
}
}
/// <summary>
/// Additional FOV scale for OMR's dedicated viewmodel camera. The authored
/// value blends with the same smooth start/full window used by physical
/// viewmodel aim alignment so depth offset and projection can move together.
/// </summary>
public float CurrentViewModelAimFovScale
{
get
{
if ( !HasOpticProfile )
return 1f;
float fullScale = MathX.Clamp(
OpticProfile.ViewModelAimFovScale,
0.5f,
1.5f
);
return MathX.Lerp(
1f,
fullScale,
ResolveViewModelAimFovBlend()
);
}
}
public bool HideCrosshairWhileAiming =>
!HasOpticProfile ||
OpticProfile.HideCrosshairWhileAiming;
/// <summary>
/// True when this optic is using the real picture-in-picture presentation.
/// Optical magnification belongs exclusively to OMRScopePiPCamera; the normal
/// gameplay camera may optionally receive only the separate peripheral-focus
/// FOV scale authored by the optic profile.
/// </summary>
public bool UsesPictureInPictureScope =>
HasOpticProfile &&
OpticProfile.Mode == OMROpticMode.MagnifiedScope &&
(
OpticProfile.PresentationMode == OMROpticPresentationMode.PictureInPicture ||
OpticProfile.PresentationMode == OMROpticPresentationMode.ModelLensPictureInPicture
);
private float ResolveViewModelAimFovBlend()
{
if ( !HasOpticProfile )
return AimFraction;
float start = MathX.Clamp(
OpticProfile.ViewModelAimAlignmentStart,
0f,
1f
);
float full = MathX.Clamp(
OpticProfile.ViewModelAimAlignmentFull,
start,
1f
);
float t;
if ( full <= start + 0.0001f )
{
t = AimFraction >= full ? 1f : 0f;
}
else
{
t = MathX.Clamp(
(AimFraction - start) / (full - start),
0f,
1f
);
}
return t * t * (3f - 2f * t);
}
private void UpdateAimTransitionState()
{
float target = IsAiming ? 1f : 0f;
float transitionTime = MathF.Max( AimFovTransitionTime, 0f );
if ( transitionTime <= 0f )
{
_aimFovBlend = target;
return;
}
float direction = IsAiming ? 1f : -1f;
_aimFovBlend += direction * Time.Delta / transitionTime;
_aimFovBlend = MathX.Clamp( _aimFovBlend, 0f, 1f );
}
private float ResolveAimAccuracyBlend()
{
if ( !IsAiming )
return 0f;
if (
!HasOpticProfile ||
!OpticProfile.UseAimProgressForAccuracy
)
{
return 1f;
}
float start = MathX.Clamp(
OpticProfile.AccuracyBlendStart,
0f,
1f
);
float full = MathX.Clamp(
OpticProfile.AccuracyFullThreshold,
start,
1f
);
if ( AimFraction <= start )
return 0f;
if ( full <= start + 0.0001f )
return 1f;
return MathX.Clamp(
(AimFraction - start) / (full - start),
0f,
1f
);
}
}