OneMoreRoundWeapon.Cycling.cs
using Sandbox;
using System;
/// <summary>
/// Generic post-shot manual cycling capability. Bolt-action is the first use,
/// but the mechanics intentionally understand only "cycle" so pump/lever
/// weapons can reuse the same action gate later.
/// </summary>
public partial class OneMoreRoundWeapon
{
[Property, Group( "Weapon Cycling" ), Description( "Optional reusable cycling profile. When a Weapon Definition is assigned, its profile becomes canonical during initialization." )]
public OMRWeaponCycleProfile CycleProfile { get; set; }
private bool _cyclePending;
private bool _cyclePresentationStarted;
private float _cyclePresentationStartAt = -1f;
private float _cycleCompleteAt = -1f;
private bool _cycleStartedWhileAiming;
public bool HasCycleProfile =>
CycleProfile is not null &&
CycleProfile.IsValid;
public bool IsChambered { get; private set; } = true;
public bool IsCyclePendingOrActive =>
HasCycleProfile &&
_cyclePending;
public bool CycleStartedWhileAiming => _cycleStartedWhileAiming;
public float CycleProgress
{
get
{
if ( !IsCyclePendingOrActive )
return IsChambered ? 1f : 0f;
float duration = MathF.Max( CycleProfile.Duration, 0f );
if ( duration <= 0f )
return RealTime.Now >= _cyclePresentationStartAt ? 1f : 0f;
return MathX.Clamp(
(RealTime.Now - _cyclePresentationStartAt) / duration,
0f,
1f
);
}
}
internal bool IsPrimaryBlockedByCycle() =>
HasCycleProfile &&
(_cyclePending || !IsChambered);
internal bool CanAimDuringCurrentCycle()
{
if ( !IsCyclePendingOrActive )
return true;
// StartDelay belongs to the just-fired shot, not to the visible bolt/pump
// action. Keep the shot's ADS state valid until the authored cycle begins.
if ( !_cyclePresentationStarted )
return true;
return IsAimAllowedDuringActiveCycle();
}
private bool IsAimAllowedDuringActiveCycle()
{
if ( !HasCycleProfile )
return true;
return CycleProfile.AimPolicy switch
{
OMRCycleAimPolicy.ForceExit => false,
OMRCycleAimPolicy.RetainIfAlreadyAiming => _cycleStartedWhileAiming,
OMRCycleAimPolicy.Allow => true,
_ => CycleProfile.AllowAimDuringCycle
};
}
private void BeginPostShotCycle()
{
if (
!HasCycleProfile ||
!CycleProfile.AutoCycleAfterPrimaryShot
)
{
return;
}
float now = RealTime.Now;
float startDelay = MathF.Max( CycleProfile.StartDelay, 0f );
float duration = MathF.Max( CycleProfile.Duration, 0f );
IsChambered = false;
_cycleStartedWhileAiming = IsAiming;
_cyclePending = true;
_cyclePresentationStarted = false;
_cyclePresentationStartAt = now + startDelay;
_cycleCompleteAt = _cyclePresentationStartAt + duration;
// Keep the shot's ADS pose alive through StartDelay. The visible/mechanical
// cycle has not started yet, so forcing ADS off here can also restore the
// full-strength fire animation while b_attack is still in flight. ADS is
// released at StartCyclePresentation instead.
// Zero-delay profiles should expose their presentation event immediately.
if ( startDelay <= 0f )
{
StartCyclePresentation();
}
}
private void UpdateCycleRuntimeState()
{
if ( !_cyclePending )
return;
if ( !HasCycleProfile )
{
ResetCycleState( chambered: true );
return;
}
float now = RealTime.Now;
if (
!_cyclePresentationStarted &&
now >= _cyclePresentationStartAt
)
{
StartCyclePresentation();
}
if ( now >= _cycleCompleteAt )
{
CompleteCycle();
}
}
private void StartCyclePresentation()
{
if ( _cyclePresentationStarted )
return;
_cyclePresentationStarted = true;
if ( !IsAimAllowedDuringActiveCycle() && IsAiming )
{
SetAiming( false );
}
SetMechanicalActionState( OMRWeaponActionState.Cycling );
}
private void CompleteCycle()
{
if ( !_cyclePending )
return;
_cyclePending = false;
IsChambered = true;
_cycleStartedWhileAiming = false;
_cyclePresentationStartAt = -1f;
_cycleCompleteAt = -1f;
if ( _cyclePresentationStarted )
{
_cyclePresentationStarted = false;
ClearMechanicalActionState( OMRWeaponActionState.Cycling );
}
}
private void ResetCycleState( bool chambered )
{
_cyclePending = false;
_cyclePresentationStarted = false;
_cycleStartedWhileAiming = false;
_cyclePresentationStartAt = -1f;
_cycleCompleteAt = -1f;
IsChambered = chambered;
// A capability can disappear during hotload/authoring. Never leave the
// generic mechanical gate stranded in Cycling when that happens.
if ( _mechanicalActionState == OMRWeaponActionState.Cycling )
{
_mechanicalActionState = OMRWeaponActionState.Ready;
}
}
public override bool CanReload()
{
if ( !AllowReload )
return false;
if (
HasCycleProfile &&
CycleProfile.BlockReloadDuringCycle &&
IsCyclePendingOrActive
)
{
return false;
}
return base.CanReload();
}
internal float GetCycleMinimumFireInterval()
{
if (
!HasCycleProfile ||
!CycleProfile.AutoCycleAfterPrimaryShot
)
{
return 0f;
}
return CycleProfile.TotalLockoutTime;
}
}