OMROpticProfile.cs
using Sandbox;
/// <summary>
/// Reusable authored optic profile.
///
/// FOV/transition values become the weapon's authored handling baseline when
/// the profile is assigned through OMRWeaponDefinition. Runtime stat modifiers
/// may still transform those resolved handling values afterwards.
///
/// Scope rendering remains a presentation concern. This resource declares what
/// presentation should be used and the authored values required by that
/// presentation; weapon mechanics remain independent of the renderer.
/// </summary>
[AssetType(
Name = "OMR Optic Profile",
Extension = "omropt",
Category = "One More Round/Weapons"
)]
public sealed partial class OMROpticProfile : GameResource
{
[Group( "Identity" ), Title( "Optic ID" )]
public string OpticId { get; set; } = string.Empty;
[Group( "Identity" )]
public OMROpticMode Mode { get; set; } = OMROpticMode.IronSights;
[Group( "Identity" ), Range( 1f, 20f ), Description( "Authored optical magnification. PiP scopes can use this value to derive an exact angular FOV instead of relying on a hand-tuned FOV multiplier." )]
public float NominalMagnification { get; set; } = 1f;
[Group( "ADS" ), Range( 0.1f, 1f ), Description( "Multiplier applied to the ordinary gameplay-camera FOV at full ADS for non-PiP weapons, and retained as the legacy/fallback PiP optic FOV model. PiP optical magnification should normally use Nominal Magnification instead." )]
public float AimFovScale { get; set; } = 0.8f;
[Group( "ADS" ), Title( "Peripheral Aim FOV Scale" ), Range( 0.5f, 1.5f ), Description( "Additional full-ADS FOV scale applied only to the gameplay/peripheral camera while using a PiP magnified scope. 1 preserves the current 1x periphery; values below 1 add a subtle peripheral focus zoom. This presentation scale does not change the authored PiP optical magnification." )]
public float PeripheralAimFovScale { get; set; } = 1f;
[Group( "ADS" ), Range( 0f, 2f ), Description( "Seconds to move between hip and full ADS." )]
public float AimTransitionTime { get; set; } = 0.15f;
[Group( "ADS" ), Range( 0.1f, 4f ), Description( "Speed passed to the existing ironsights/viewmodel animation parameter." )]
public float AimAnimationSpeed { get; set; } = 1f;
[Group( "Sensitivity" ), Range( 0.1f, 2f ), Description( "Additional look-sensitivity multiplier blended in with ADS. This is applied on top of OMR's FOV-based zoom matching, including the PiP scope FOV." )]
public float ScopedSensitivityMultiplier { get; set; } = 1f;
[Group( "Precision" ), Description( "When enabled, ADS accuracy follows AimFraction instead of becoming fully active on the first frame aim is held." )]
public bool UseAimProgressForAccuracy { get; set; } = false;
[Group( "Precision" ), Range( 0f, 1f ), Description( "AimFraction where the weapon begins blending toward AimSpreadMultiplier." )]
public float AccuracyBlendStart { get; set; } = 0.55f;
[Group( "Precision" ), Range( 0f, 1f ), Description( "AimFraction where AimSpreadMultiplier is fully applied." )]
public float AccuracyFullThreshold { get; set; } = 0.95f;
[Group( "Presentation" ), Description( "How this optic should be presented once Scope Presentation Threshold is reached." )]
public OMROpticPresentationMode PresentationMode { get; set; } = OMROpticPresentationMode.None;
[Group( "Presentation" ), Range( 0f, 1f ), Description( "AimFraction where magnified-scope presentation may become active." )]
public float ScopePresentationThreshold { get; set; } = 0.88f;
[Group( "Presentation" ), Description( "Hide the normal OMR crosshair while aim is held. Scope/reticle presentation may provide its own reticle." )]
public bool HideCrosshairWhileAiming { get; set; } = true;
// =========================================================
// VIEWMODEL ADS ALIGNMENT
// =========================================================
[Group( "Viewmodel ADS Alignment" ), Description( "Apply a presentation-only correction to the owner viewmodel while aiming so an optic/lens anchor can be centered independently of the weapon's authored default ironsights pose. Facepunch first-person weapon docs explicitly leave attachment-specific ironsight offsets to game code." )]
public bool EnableViewModelAimAlignment { get; set; } = false;
[Group( "Viewmodel ADS Alignment" ), Description( "Name of the GameObject inside the spawned viewmodel whose center should be aligned toward the gameplay camera. For the M700 model-space scope this is ScopeLensSurface." )]
public string ViewModelAimAnchorName { get; set; } = "ScopeLensSurface";
[Group( "Viewmodel ADS Alignment" ), Range( 0f, 1f ), Description( "AimFraction where attachment-specific viewmodel correction begins blending in. The authored ironsights animation still provides the base ADS motion." )]
public float ViewModelAimAlignmentStart { get; set; } = 0.05f;
[Group( "Viewmodel ADS Alignment" ), Range( 0f, 1f ), Description( "AimFraction where the correction reaches full strength. Magnified scopes normally want this at or before Scope Presentation Threshold so the lens is centered before the PiP feed appears." )]
public float ViewModelAimAlignmentFull { get; set; } = 0.90f;
[Group( "Viewmodel ADS Alignment" ), Description( "Desired optic-center offset in the gameplay camera plane, in world units. X = right, Y = up. Leave at 0,0 for a perfectly centered scope; small values can reproduce deliberately off-center weapon presentation." )]
public Vector2 ViewModelAimScreenOffset { get; set; } = Vector2.Zero;
[Group( "Viewmodel ADS Alignment" ), Title( "ViewModel Aim FOV Scale" ), Range( 0.5f, 1.5f ), Description( "Additional FOV multiplier for OMR's dedicated viewmodel camera at full ADS. 1 preserves the existing viewmodel projection; values below 1 narrow the viewmodel FOV and make the weapon/scope appear larger without moving it closer to the eye. The scale blends using the same start/full thresholds as viewmodel aim alignment." )]
public float ViewModelAimFovScale { get; set; } = 1f;
[Group( "Viewmodel ADS Alignment" ), Range( -10f, 10f ), Description( "Additional translation along the gameplay camera forward axis at full ADS. Negative moves the whole viewmodel toward the player's eye (larger-looking scope); positive moves it farther away. The authored ADS animation still determines the base eye relief." )]
public float ViewModelAimDepthOffset { get; set; } = 0f;
[Group( "Viewmodel Fire Safety" ), Range( 0f, 1f ), Description( "Seconds to keep ironsights_fire_scale latched to the scoped value after an ADS shot. Useful for bolt/pump weapons that temporarily leave ADS after firing; prevents the AnimGraph from restoring full-strength b_attack motion while that same fire animation is still playing." )]
public float ViewModelFireScaleHoldTime { get; set; } = 0f;
[Group( "Viewmodel Eye Relief" ), Description( "Capture the optic's forward distance once ADS settles and prevent later viewmodel animation from pulling the optic closer to the gameplay camera than that baseline. This is presentation-only and is useful for physical scopes with very short eye relief." )]
public bool ProtectViewModelEyeRelief { get; set; } = false;
[Group( "Viewmodel Eye Relief" ), Range( 0f, 5f ), Description( "How much backward travel toward the eye is allowed after the ADS eye-relief baseline is captured. 0 locks the minimum distance exactly; a small positive value preserves some authored kick without allowing camera clipping." )]
public float ViewModelEyeReliefTolerance { get; set; } = 0f;
[Group( "Viewmodel Eye Relief" ), Range( 0f, 1f ), Description( "AimFraction where the current optic depth is captured as the safe eye-relief baseline. Usually at or just after full viewmodel alignment." )]
public float ViewModelEyeReliefCaptureThreshold { get; set; } = 0.95f;
// =========================================================
// PICTURE IN PICTURE CAMERA
// =========================================================
[Group( "PiP Camera" ), Description( "Where the secondary scope camera obtains its transform. Model-space sniper scopes should normally use ViewModel Anchor Position + Aim Rotation." )]
public OMRPiPCameraPoseMode PiPCameraPoseMode { get; set; } =
OMRPiPCameraPoseMode.MainCamera;
[Group( "PiP Camera" ), Description( "Name of the GameObject inside the active viewmodel that marks the optical-camera position. Used by the ViewModel Anchor camera modes." )]
public string PiPCameraAnchorName { get; set; } = "ScopeCameraAnchor";
[Group( "PiP Camera Position" ), Description( "Keep the off-eye preview physically located at the authored scope anchor, then blend the render camera toward the gameplay eye during ADS. This preserves physical parallax in hip-fire while making the fully aimed sight picture stable and gameplay-authoritative." )]
public bool BlendPiPPositionToGameplayEye { get; set; } = false;
[Group( "PiP Camera Position" ), Range( 0f, 1f ), Description( "AimFraction where the PiP camera starts moving from the physical scope anchor toward the gameplay eye." )]
public float PiPPositionBlendStart { get; set; } = 0.45f;
[Group( "PiP Camera Position" ), Range( 0f, 1f ), Description( "AimFraction where the PiP camera has fully reached the gameplay eye position." )]
public float PiPPositionBlendFull { get; set; } = 0.95f;
[Group( "PiP Camera" ), Description( "Allow post-processing on the secondary scope camera. Keep this disabled unless the optic specifically needs it; a second post-process pass is more expensive." )]
public bool PiPEnablePostProcessing { get; set; } = false;
[Group( "PiP Camera" ), Description( "Derive the PiP camera FOV from Nominal Magnification using angular projection math. Recommended for magnified optics; Aim FOV Scale remains available as the legacy/fallback model." )]
public bool UseNominalMagnificationForPiP { get; set; } = false;
[Group( "PiP Camera" ), Description( "Blend With Aim ramps magnification during ADS. Constant Optical keeps a fixed-power optic magnified even in hip-fire preview and uses the lens simulation/eye-box transition to communicate alignment." )]
public OMRPiPMagnificationMode PiPMagnificationMode { get; set; } = OMRPiPMagnificationMode.BlendWithAim;
[Group( "PiP Camera" ), Description( "Apply authored optical magnification as a centered ViewSetup.ClipSpaceBounds crop inside RenderToTexture. The crop is applied before the texture reaches the lens presentation." )]
public bool UsePresentationCropForMagnification { get; set; } = false;
[Group( "PiP Apparent Magnification" ), Description( "Compensate the render-space crop for the fact that a model-space lens occupies only a fraction of the player's screen. Without this, a mathematically 4x secondary camera shown in a lens that is about one quarter of the screen width appears close to 1x on the monitor." )]
public bool CompensateForModelLensScreenSize { get; set; } = false;
[Group( "PiP Apparent Magnification" ), Description( "Measure the actual physical model-space lens diameter from the ScopeImagePlane renderer and the composed gameplay camera. Older WorldPanel optics remain supported as a compatibility fallback. This keeps apparent magnification stable when the lens mesh is resized/repositioned or changes apparent size during ADS." )]
public bool AutoMeasureModelLensScreenFraction { get; set; } = false;
[Group( "PiP Apparent Magnification" ), Range( 0.5f, 1.5f ), Description( "Calibration multiplier applied to the automatically measured physical lens diameter. Keep at 1 when ScopeImagePlane fills the usable circular aperture; lower it slightly only if the visible inner glass is smaller than the image-plane bounds." )]
public float AutoMeasuredLensFractionScale { get; set; } = 1f;
[Group( "PiP Apparent Magnification" ), Range( 0f, 40f ), Description( "Exponential smoothing speed for automatic lens-size measurement. 0 disables smoothing. A modest value prevents tiny viewmodel/bob changes from causing visible zoom pumping while still following the ADS transition." )]
public float AutoMeasuredLensFractionSmoothingSpeed { get; set; } = 18f;
[Group( "PiP Apparent Magnification" ), Range( 30f, 140f ), Description( "Horizontal gameplay FOV at which the authored lens screen fractions below were measured." )]
public float LensFractionReferenceFov { get; set; } = 85f;
[Group( "PiP Apparent Magnification" ), Range( 0.02f, 0.8f ), Description( "Approximate inner-lens diameter as a fraction of screen width in the hip-fire pose at Lens Fraction Reference FOV." )]
public float HipLensScreenFraction { get; set; } = 0.10f;
[Group( "PiP Apparent Magnification" ), Range( 0.02f, 0.8f ), Description( "Approximate inner-lens diameter as a fraction of screen width at full ADS at Lens Fraction Reference FOV." )]
public float AdsLensScreenFraction { get; set; } = 0.25f;
[Group( "PiP Apparent Magnification" ), Range( 0.5f, 8f ), Description( "How magnified the tiny off-eye/hip-fire sight picture should look relative to the surrounding world. This can stay near 1x while the optic transitions toward its nominal magnification in ADS." )]
public float HipApparentMagnification { get; set; } = 1f;
[Group( "PiP Apparent Magnification" ), Range( 0f, 1f ), Description( "AimFraction where the visible magnification begins moving from Hip Apparent Magnification toward Nominal Magnification." )]
public float ApparentMagnificationBlendStart { get; set; } = 0.15f;
[Group( "PiP Apparent Magnification" ), Range( 0f, 1f ), Description( "AimFraction where the model-space lens reaches its full authored apparent magnification." )]
public float ApparentMagnificationBlendFull { get; set; } = 0.90f;
[Group( "PiP Camera" ), Description( "Keep the PiP camera alive whenever this optic is equipped, even before full ADS. This makes a physical scope behave like glass instead of a screen that suddenly powers on at the presentation threshold." )]
public bool KeepPiPWarmWhileEquipped { get; set; } = false;
[Group( "PiP Camera" ), Range( 128, 1024 ), Description( "Render-target resolution used while the scope is prewarmed but not yet close to full ADS. The lens is small on screen in this state, so a cheaper target is normally sufficient." )]
public int PiPPrewarmResolution { get; set; } = 320;
[Group( "PiP Camera" ), Range( 0f, 1f ), Description( "AimFraction where the PiP target upgrades from the prewarm resolution to the normal authored resolution. The output texture remains visible during the transition." )]
public float PiPFullResolutionAimFraction { get; set; } = 0.72f;
// =========================================================
// LENS SIMULATION
// =========================================================
[Group( "Lens Simulation" ), Description( "Apply a lightweight optical treatment to the PiP image: reduced off-eye clarity, tint, edge darkening and a subtle coated-glass reflection. This is UI-based and intentionally precedes the future physical lens shader." )]
public bool EnableLensSimulation { get; set; } = false;
[Group( "Lens Simulation" ), Range( 0f, 1f ), Description( "AimFraction where optical clarity begins improving." )]
public float LensClarityStart { get; set; } = 0.05f;
[Group( "Lens Simulation" ), Range( 0f, 1f ), Description( "AimFraction where the lens reaches its fully aligned optical presentation." )]
public float LensClarityFull { get; set; } = 0.92f;
[Group( "Lens Simulation" ), Range( 0f, 6f )]
public float LensHipBlur { get; set; } = 0.75f;
[Group( "Lens Simulation" ), Range( 0f, 3f )]
public float LensAdsBlur { get; set; } = 0f;
[Group( "Lens Simulation" ), Range( 0.25f, 1.5f )]
public float LensHipBrightness { get; set; } = 0.82f;
[Group( "Lens Simulation" ), Range( 0.25f, 1.5f )]
public float LensAdsBrightness { get; set; } = 0.98f;
[Group( "Lens Simulation" ), Range( 0f, 1.5f )]
public float LensHipSaturation { get; set; } = 0.72f;
[Group( "Lens Simulation" ), Range( 0f, 1.5f )]
public float LensAdsSaturation { get; set; } = 0.94f;
[Group( "Lens Simulation" ), Range( 0.25f, 1.5f )]
public float LensHipContrast { get; set; } = 0.92f;
[Group( "Lens Simulation" ), Range( 0.25f, 1.5f )]
public float LensAdsContrast { get; set; } = 1f;
[Group( "Lens Simulation" ), Description( "Subtle coating/tint colour layered over the lens feed." )]
public Color LensTintColor { get; set; } = new Color( 0.58f, 0.70f, 0.74f, 1f );
[Group( "Lens Simulation" ), Range( 0f, 0.75f )]
public float LensHipTintOpacity { get; set; } = 0.14f;
[Group( "Lens Simulation" ), Range( 0f, 0.5f )]
public float LensAdsTintOpacity { get; set; } = 0.045f;
[Group( "Lens Simulation" ), Range( 0f, 1f ), Description( "Darkening at the edge of the lens when the eye is outside the ideal eye box." )]
public float LensHipEdgeDarkening { get; set; } = 0.58f;
[Group( "Lens Simulation" ), Range( 0f, 1f )]
public float LensAdsEdgeDarkening { get; set; } = 0.14f;
[Group( "Lens Simulation" ), Range( 0f, 0.75f ), Description( "Strength of the broad coated-glass highlight while off-eye." )]
public float LensHipReflectionOpacity { get; set; } = 0.14f;
[Group( "Lens Simulation" ), Range( 0f, 0.5f )]
public float LensAdsReflectionOpacity { get; set; } = 0.035f;
[Group( "Lens Simulation" ), Description( "Presentation transform applied to the raw PiP texture while off-eye / in hip-fire preview. Upright means no additional transform, so it can deliberately preserve the render target's raw inverted/mirrored look when desired." )]
public OMRScopePreviewOrientation HipPreviewOrientation { get; set; } = OMRScopePreviewOrientation.Upright;
[Group( "Lens Simulation" ), Description( "Presentation transform applied to the PiP texture at aligned ADS. Use this to erect a deliberately inverted hip-fire preview without changing the camera or ballistic aim." )]
public OMRScopePreviewOrientation AdsPreviewOrientation { get; set; } = OMRScopePreviewOrientation.Upright;
[Group( "Lens Simulation" ), Range( 0f, 1f ), Description( "AimFraction where the off-eye preview begins crossfading toward the final upright sight picture." )]
public float LensErectStart { get; set; } = 0.45f;
[Group( "Lens Simulation" ), Range( 0f, 1f ), Description( "AimFraction where the final upright sight picture is fully established." )]
public float LensErectFull { get; set; } = 0.88f;
[Group( "Lens Simulation" ), Range( 0f, 0.75f ), Description( "Temporary darkening at the midpoint of the inverted-to-upright crossfade. This hides the otherwise game-like double exposure and reads more like the eye entering the optic's eye box." )]
public float LensErectTransitionDarkening { get; set; } = 0.18f;
// =========================================================
// EYE BOX / SCOPE SHADOW
// =========================================================
[Group( "Eye Box" ), Description( "Drive scope shadow from the physical angular misalignment between the gameplay eye and the authored optic anchor. The magnification and ballistic aim remain unchanged; this is presentation only." )]
public bool EnableEyeBoxSimulation { get; set; } = false;
[Group( "Eye Box" ), Range( 0f, 10f ), Description( "Angular error in degrees that is still treated as perfectly aligned." )]
public float EyeBoxPerfectAngleDegrees { get; set; } = 1f;
[Group( "Eye Box" ), Range( 1f, 45f ), Description( "Angular error in degrees where the additional off-axis scope shadow reaches full strength." )]
public float EyeBoxFullShadowAngleDegrees { get; set; } = 14f;
[Group( "Eye Box" ), Range( 0f, 1f ), Description( "Maximum extra edge darkening contributed by eye-box misalignment. This composes with the normal Lens Hip/ADS Edge Darkening values." )]
public float EyeBoxShadowStrength { get; set; } = 0.45f;
[Group( "Eye Box" ), Range( 0f, 35f ), Description( "Maximum percentage-point shift of the clear vignette center toward the apparent exit pupil as the eye moves off-axis." )]
public float EyeBoxGradientShiftPercent { get; set; } = 18f;
// =========================================================
// PICTURE IN PICTURE RESOLUTION
// =========================================================
[Group( "PiP Resolution" ), Description( "Fixed uses PiP Render Resolution. Screen Relative scales with the user's current display height and only reallocates when the quantized target size changes." )]
public OMRPiPResolutionMode PiPResolutionMode { get; set; } =
OMRPiPResolutionMode.Fixed;
[Group( "PiP Resolution" ), Range( 256, 2048 ), Description( "Square render-target resolution when Resolution Mode is Fixed." )]
public int PiPRenderResolution { get; set; } = 768;
[Group( "PiP Resolution" ), Range( 0.15f, 1.5f ), Description( "For Screen Relative mode, desired render-target width/height as a fraction of current screen height. 0.55 gives roughly 640px at 1080p, 832px at 1440p and 1216px at 4K before min/max clamping." )]
public float PiPScreenHeightScale { get; set; } = 0.55f;
[Group( "PiP Resolution" ), Range( 256, 2048 )]
public int PiPMinimumResolution { get; set; } = 512;
[Group( "PiP Resolution" ), Range( 256, 4096 )]
public int PiPMaximumResolution { get; set; } = 1536;
[Group( "PiP Resolution" ), Range( 16, 256 ), Description( "Screen-relative targets are rounded upward to this many pixels so resizing does not recreate the render target for every tiny viewport change." )]
public int PiPResolutionQuantum { get; set; } = 64;
// =========================================================
// SCREEN-SPACE PIP DEBUG/FALLBACK PRESENTATION
// =========================================================
[Group( "Model Lens Hybrid Overlay" ), Description( "When enabled for ModelLensPictureInPicture optics, OMRScopeHUD also draws the PiP feed/reticle in screen space at the center of the viewport while the physical scope housing remains on the weapon. This is a reliable fallback while the final authored lens-material path is still being developed." )]
public bool UseScreenSpaceHybridOverlay { get; set; } = false;
[Group( "Model Lens Hybrid Overlay" ), Range( 10f, 95f ), Description( "Diameter of the centered screen-space hybrid overlay, as a percentage of viewport height." )]
public float HybridOverlayDiameterVh { get; set; } = 40f;
[Group( "Model Lens Hybrid Overlay" ), Description( "Draw the procedural ring around the hybrid screen-space overlay. Usually false when the physical scope model already supplies the ring/housing." )]
public bool HybridOverlayDrawRing { get; set; } = false;
[Group( "Model Lens Hybrid Overlay" ), Range( 0f, 1f ), Description( "AimFraction where the centered hybrid overlay begins fading in. The physical model-space lens may already be showing the prewarmed PiP feed before this point." )]
public float HybridOverlayFadeStart { get; set; } = 0.78f;
[Group( "Model Lens Hybrid Overlay" ), Range( 0f, 1f ), Description( "AimFraction where the centered hybrid overlay reaches full opacity." )]
public float HybridOverlayFadeFull { get; set; } = 0.94f;
[Group( "Screen PiP Fallback" ), Range( 20f, 95f ), Description( "Diameter of the visible screen-space PiP lens as a percentage of viewport height. Ignored by model-space lens presentation." )]
public float PiPLensDiameterVh { get; set; } = 64f;
[Group( "Screen PiP Fallback" ), Range( 0f, 0.85f ), Description( "Optional dimming of peripheral vision for the screen-space PiP fallback. Model-space lens presentation normally leaves this at zero." )]
public float PiPPeripheralDimOpacity { get; set; } = 0.12f;
[Group( "Screen PiP Fallback" ), Range( 0f, 12f )]
public float PiPRingThickness { get; set; } = 3f;
[Group( "Screen PiP Fallback" )]
public Color PiPRingColor { get; set; } = Color.Black;
// =========================================================
// RETICLE
// =========================================================
[Group( "Scope Reticle" ), Description( "Draw the built-in procedural scope reticle on PiP scope presentations." )]
public bool ShowSimpleReticle { get; set; } = true;
[Group( "Scope Reticle" ), Range( 0f, 1f ), Description( "Reticle opacity while the eye is off-axis / in hip-fire preview." )]
public float ReticleHipOpacity { get; set; } = 0.12f;
[Group( "Scope Reticle" ), Range( 0f, 1f ), Description( "Reticle opacity at full ADS." )]
public float ReticleAdsOpacity { get; set; } = 1f;
[Group( "Scope Reticle" ), Range( 0f, 4f ), Description( "Optional contrasting outline around the procedural reticle. Useful against both bright and dark PiP scenes while keeping the center mark thin." )]
public float SimpleReticleOutlineWidth { get; set; } = 0.75f;
[Group( "Scope Reticle" ), Range( 0f, 1f )]
public float SimpleReticleOutlineOpacity { get; set; } = 0.35f;
[Group( "Scope Reticle" )]
public OMRScopeReticleStyle SimpleReticleStyle { get; set; } = OMRScopeReticleStyle.FineCross;
[Group( "Scope Reticle" )]
public Color SimpleReticleColor { get; set; } = Color.Black;
[Group( "Scope Reticle" ), Range( 0.5f, 8f )]
public float SimpleReticleThickness { get; set; } = 1.5f;
[Group( "Scope Reticle" ), Range( 0f, 64f )]
public float SimpleReticleGap { get; set; } = 8f;
[Group( "Scope Reticle" ), Range( 8f, 256f )]
public float SimpleReticleLineLength { get; set; } = 72f;
[Group( "Scope Reticle" ), Range( 0f, 16f )]
public float SimpleReticleDotSize { get; set; } = 3f;
// Legacy fallback fields retained so existing .omropt resources deserialize
// cleanly. Phase 5D+ no longer relies on the procedural SimpleOverlay mask.
[Group( "Legacy Simple Overlay" ), Hide]
public float SimpleOverlayDiameterVh { get; set; } = 64f;
[Group( "Legacy Simple Overlay" ), Hide]
public float SimpleOverlayOuterOpacity { get; set; } = 0.94f;
[Group( "Legacy Simple Overlay" ), Hide]
public float SimpleOverlayRingThickness { get; set; } = 2f;
[Group( "Legacy Simple Overlay" ), Hide]
public Color SimpleOverlayRingColor { get; set; } = Color.Black;
}