manager/Manager.TimeScale.cs

Partial Manager class handling game time scaling and pause behavior. It computes TimeScale based on spectator state, player choosing/level-up timers, pause/gameover states, and manages prioritized time-scale requests that lerp over time.

Networking
using System;

public partial class Manager
{
	[Sync] public bool IsPaused { get; private set; }

	private Player GetDisplayedChoicePlayer()
	{
		if ( IsSpectator && SelectedPlayer.IsValid() )
			return SelectedPlayer;

		return LocalPlayer;
	}

	public bool IsPausedForChoosing
	{
		get
		{
			var player = GetDisplayedChoicePlayer();
			return !IsUnpausedChoosing && player.IsValid() && player.IsChoosingLevelUpReward;
		}
	}

	[Sync] public float TimeScaleSync { get; set; }
	private float _timeScaleBeforePause;

	private class TimeScaleRequest
	{
		public float StartTimeScale;
		public float EndTimeScale;
		public float Duration;
		public int Priority;
		public double RealStartTime;
	}


	private List<TimeScaleRequest> _timeScaleRequests = new();

	private float GetDisplayedPerkCount( Player player )
	{
		if ( !player.IsValid() )
			return 0f;

		return player.IsProxy ? player.SyncPerks.Count : player.Perks.Count;
	}

	private float GetChoiceTimeScale( Player player )
	{
		if ( !player.IsValid() )
			return TimeScaleSync;

		if ( player.IsChoosingLevelUpReward )
			return player.RealTimeSinceLvlUp > 0.5f ? 0f : Utils.Map( player.RealTimeSinceLvlUp, 0f, 0.5f, 0.7f, 0f, EasingType.SineOut );

		return GetDisplayedPerkCount( player ) > 1
			? (player.RealTimeSinceChosePerk > 0.5f ? 1f : Utils.Map( player.RealTimeSinceChosePerk, 0f, 0.5f, 0f, 1f, EasingType.SineOut ))
			: TimeScaleSync;
	}

	void HandleTimeScale()
	{
		//if( Game.IsEditor && Networking.IsHost )
		//	TimeScaleSync = Input.Keyboard.Down( "G" ) ? 0.1f : 1f;

		if ( IsSpectator )
		{
			var watchedPlayer = GetDisplayedChoicePlayer();
			if ( IsPaused )
				Scene.TimeScale = TimeScaleSync;
			else if ( IsGameOver )
				Scene.TimeScale = Utils.DynamicEaseTo( Scene.TimeScale, 0.1f, 0.1f, Time.Delta );
			else if ( !IsUnpausedChoosing )
				Scene.TimeScale = GetChoiceTimeScale( watchedPlayer );
			else
				Scene.TimeScale = TimeScaleSync;
		}
		else if ( !IsUnpausedChoosing && !IsPaused && !IsGameOver && LocalPlayer.IsValid() )
		{
			Scene.TimeScale = GetChoiceTimeScale( LocalPlayer );
		}
		else if ( IsPaused )
		{
			if ( !Networking.IsHost )
			{
				Scene.TimeScale = TimeScaleSync;
			}
		}
		else if( IsGameOver )
		{
			Scene.TimeScale = Utils.DynamicEaseTo( Scene.TimeScale, 0.1f, 0.1f, Time.Delta );
		}
		else
		{
			HandleTimeScaleRequests();

			Scene.TimeScale = TimeScaleSync;
		}
	}

	/// <summary>
	/// Request a change to TimeScaleSync over a duration, with a priority.
	/// If isInstant is true, sets the value immediately (no lerp).
	/// You can specify the initial value for lerping.
	/// </summary>
	public void RequestTimeScale( float startTimeScale, float endTimeScale, float duration, int priority )
	{
		if ( !Networking.IsHost ) 
			return;

		_timeScaleRequests.Add( new TimeScaleRequest
		{
			StartTimeScale = startTimeScale,
			EndTimeScale = endTimeScale,
			Duration = duration,
			Priority = priority,
			RealStartTime = RealTime.Now
		} );
	}

	private void HandleTimeScaleRequests()
	{
		if ( _timeScaleRequests.Count == 0 )
		{
			if ( !IsGameOver )
				TimeScaleSync = 1f;

			return;
		}

		var activeRequests = _timeScaleRequests
			.Where( r => (RealTime.Now - r.RealStartTime) < r.Duration )
			.ToList();

		_timeScaleRequests.RemoveAll( r => (RealTime.Now - r.RealStartTime) >= r.Duration );

		if ( activeRequests.Count == 0 )
		{
			TimeScaleSync = 1f;
			return;
		}

		var request = activeRequests
			.OrderByDescending( r => r.Priority )
			.ThenByDescending( r => r.RealStartTime )
			.First();

		float t = Math.Clamp( (float)((RealTime.Now - request.RealStartTime) / request.Duration), 0f, 1f );
		TimeScaleSync = Utils.Map( t, 0f, 1f, request.StartTimeScale, request.EndTimeScale, EasingType.SineIn );
	}
}