OMRWeaponModelPresentationAdapter.cs
using Sandbox;
using System;

/// <summary>
/// Optional model-side presentation adapter for OMR weapons.
///
/// Drop this on a viewmodel or worldmodel prefab when its AnimGraph should react
/// to generic weapon capabilities such as cycling. The weapon core never needs
/// to know a model's graph layout; this adapter translates the generic cycle
/// state into configurable AnimGraph parameter names.
///
/// Existing weapon models do not need this component. BaseWeaponModel continues
/// to own stock deploy/attack/reload/muzzle/brass/tracer presentation.
/// </summary>
public sealed class OMRWeaponModelPresentationAdapter : Component
{
	[Property, Group( "References" )]
	public SkinnedModelRenderer Renderer { get; set; }

	[Property, Group( "Cycle AnimGraph" )]
	public bool DriveCycleParameters { get; set; } = true;

	[Property, Group( "Cycle AnimGraph" ), Description( "Optional self-resetting bool trigger fired once when the visible cycle begins. Facepunch M700 uses b_reload_bolt. Leave blank when the graph uses only held/progress parameters." )]
	public string CycleTriggerParameter { get; set; } = string.Empty;

	[Property, Group( "Cycle AnimGraph" ), Description( "Boolean parameter held true while the visible cycle animation is active. Leave blank to disable." )]
	public string CycleActiveParameter { get; set; } = "b_cycle";

	[Property, Group( "Cycle AnimGraph" ), Description( "Float 0..1 parameter updated through the visible cycle. Leave blank to disable." )]
	public string CycleProgressParameter { get; set; } = "cycle_progress";

	[Property, Group( "Cycle AnimGraph" ), Description( "Float animation-speed scale. 1 means the authored animation duration matches Authored Cycle Duration." )]
	public string CycleSpeedParameter { get; set; } = "speed_cycle";

	[Property, Group( "Cycle AnimGraph" ), Description( "Integer parameter containing OMRWeaponCycleKind. Leave blank when the graph has only one cycle family." )]
	public string CycleKindParameter { get; set; } = "cycle_kind";

	[Property, Group( "Cycle AnimGraph" ), Range( 0.01f, 4f ), Description( "Native duration of the authored cycle animation at speed 1. Used to match animation speed to the mechanical cycle duration." )]
	public float AuthoredCycleDuration { get; set; } = 0.68f;

	[Property, Group( "Optic AnimGraph" )]
	public bool DriveOpticParameters { get; set; } = true;

	[Property, Group( "Optic AnimGraph" ), Description( "Boolean parameter set once a magnified optic crosses its presentation threshold. Leave blank to disable." )]
	public string ScopeActiveParameter { get; set; } = "b_scope";

	[Property, Group( "Optic AnimGraph" ), Description( "Float 0..1 parameter containing the weapon's authored ADS transition fraction. Leave blank to disable." )]
	public string ScopeFractionParameter { get; set; } = "scope_fraction";

	[Property, Group( "Diagnostics" )]
	public bool LogCyclePresentation { get; set; } = false;

	protected override void OnStart()
	{
		base.OnStart();
		ResolveRenderer();
		ResetCyclePresentation();
		ResetOpticPresentation();
	}

	public void BeginCyclePresentation(
		OMRWeaponCycleKind kind,
		float mechanicalDuration
	)
	{
		if ( !DriveCycleParameters )
			return;

		ResolveRenderer();
		if ( Renderer is null )
			return;

		SetBool( CycleTriggerParameter, true );
		SetBool( CycleActiveParameter, true );
		SetFloat( CycleProgressParameter, 0f );
		SetInt( CycleKindParameter, (int)kind );

		float duration = MathF.Max( mechanicalDuration, 0.001f );
		float authored = MathF.Max( AuthoredCycleDuration, 0.001f );
		SetFloat( CycleSpeedParameter, authored / duration );

		if ( LogCyclePresentation )
		{
			Log.Info(
				$"[OMR MODEL PRESENTATION] Cycle START | Model:{GameObject.Name} | Kind:{kind} | Duration:{duration:0.###}"
			);
		}
	}

	public void UpdateCyclePresentation( float progress )
	{
		if ( !DriveCycleParameters )
			return;

		ResolveRenderer();
		if ( Renderer is null )
			return;

		SetFloat(
			CycleProgressParameter,
			MathX.Clamp( progress, 0f, 1f )
		);
	}

	public void CompleteCyclePresentation()
	{
		if ( !DriveCycleParameters )
			return;

		ResolveRenderer();
		if ( Renderer is null )
			return;

		SetFloat( CycleProgressParameter, 1f );
		SetBool( CycleActiveParameter, false );

		if ( LogCyclePresentation )
		{
			Log.Info(
				$"[OMR MODEL PRESENTATION] Cycle COMPLETE | Model:{GameObject.Name}"
			);
		}
	}

	public void ApplyOpticPresentation(
		float aimFraction,
		bool scopeActive
	)
	{
		if ( !DriveOpticParameters )
			return;

		ResolveRenderer();
		if ( Renderer is null )
			return;

		SetFloat( ScopeFractionParameter, MathX.Clamp( aimFraction, 0f, 1f ) );
		SetBool( ScopeActiveParameter, scopeActive );
	}

	public void ResetOpticPresentation()
	{
		ResolveRenderer();
		if ( Renderer is null )
			return;

		SetFloat( ScopeFractionParameter, 0f );
		SetBool( ScopeActiveParameter, false );
	}

	public void ResetCyclePresentation()
	{
		ResolveRenderer();
		if ( Renderer is null )
			return;

		SetBool( CycleActiveParameter, false );
		SetFloat( CycleProgressParameter, 0f );
		SetFloat( CycleSpeedParameter, 1f );
	}

	private void ResolveRenderer()
	{
		if ( Renderer is not null && Renderer.IsValid )
			return;

		Renderer = GameObject.Components.Get<SkinnedModelRenderer>(
			FindMode.InSelf |
			FindMode.InDescendants
		);
	}

	private void SetBool( string parameter, bool value )
	{
		if ( string.IsNullOrWhiteSpace( parameter ) )
			return;

		Renderer.Set( parameter, value );
	}

	private void SetFloat( string parameter, float value )
	{
		if ( string.IsNullOrWhiteSpace( parameter ) )
			return;

		Renderer.Set( parameter, value );
	}

	private void SetInt( string parameter, int value )
	{
		if ( string.IsNullOrWhiteSpace( parameter ) )
			return;

		Renderer.Set( parameter, value );
	}
}