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 );
}
}