OneMoreRoundWeapon.CyclePresentation.cs
using Sandbox;
using System;

/// <summary>
/// Peer-local cycle presentation scheduler.
///
/// Mechanical cycling remains owner/gameplay state. Presentation is derived
/// from ShotFired, which S&box already distributes to every peer that should see
/// the shot. That gives remote players bolt/pump/lever presentation without a
/// second custom RPC stream.
/// </summary>
public partial class OneMoreRoundWeapon
{
	private bool _presentationCyclePending;
	private bool _presentationCycleActive;
	private float _presentationCycleStartAt = -1f;
	private float _presentationCycleCompleteAt = -1f;

	public bool IsCyclePresentationActive =>
		_presentationCycleActive;

	public float CyclePresentationProgress
	{
		get
		{
			if ( !_presentationCyclePending )
				return 0f;

			if ( !HasCycleProfile )
				return 0f;

			float duration = MathF.Max( CycleProfile.Duration, 0f );
			if ( duration <= 0f )
				return _presentationCycleActive ? 1f : 0f;

			return MathX.Clamp(
				(RealTime.Now - _presentationCycleStartAt) / duration,
				0f,
				1f
			);
		}
	}

	private void BindCyclePresentationEvents()
	{
		PresentationEvent -= HandleCyclePresentationSourceEvent;
		PresentationEvent += HandleCyclePresentationSourceEvent;
	}

	private void HandleCyclePresentationSourceEvent(
		WeaponPresentationEvent presentationEvent
	)
	{
		if (
			presentationEvent.Kind == WeaponPresentationEventKind.ShotFired &&
			HasCycleProfile &&
			CycleProfile.AutoCycleAfterPrimaryShot
		)
		{
			ScheduleCyclePresentation( presentationEvent.Time );
			return;
		}

		if (
			presentationEvent.Kind == WeaponPresentationEventKind.Holstered
		)
		{
			ResetCyclePresentationState();
		}
	}

	private void ScheduleCyclePresentation( float shotTime )
	{
		float startDelay = MathF.Max( CycleProfile.StartDelay, 0f );
		float duration = MathF.Max( CycleProfile.Duration, 0f );

		_presentationCyclePending = true;
		_presentationCycleActive = false;
		_presentationCycleStartAt = shotTime + startDelay;
		_presentationCycleCompleteAt = _presentationCycleStartAt + duration;

		if ( startDelay <= 0f )
		{
			StartScheduledCyclePresentation();
		}
	}

	private void UpdateCyclePresentationState()
	{
		if ( !_presentationCyclePending )
			return;

		if ( !HasCycleProfile )
		{
			ResetCyclePresentationState();
			return;
		}

		float now = RealTime.Now;

		if (
			!_presentationCycleActive &&
			now >= _presentationCycleStartAt
		)
		{
			StartScheduledCyclePresentation();
		}

		if (
			_presentationCycleActive &&
			now >= _presentationCycleCompleteAt
		)
		{
			CompleteScheduledCyclePresentation();
		}
	}

	private void StartScheduledCyclePresentation()
	{
		if ( _presentationCycleActive )
			return;

		_presentationCycleActive = true;
		RaiseCycleStartedPresentation();
	}

	private void CompleteScheduledCyclePresentation()
	{
		if ( !_presentationCyclePending )
			return;

		bool wasActive = _presentationCycleActive;

		_presentationCyclePending = false;
		_presentationCycleActive = false;
		_presentationCycleStartAt = -1f;
		_presentationCycleCompleteAt = -1f;

		if ( wasActive )
		{
			RaiseCycleCompletedPresentation();
		}
	}

	private void ResetCyclePresentationState()
	{
		_presentationCyclePending = false;
		_presentationCycleActive = false;
		_presentationCycleStartAt = -1f;
		_presentationCycleCompleteAt = -1f;
	}
}